diff --git a/home/dot_local/bin/executable_quick-memo.tmpl b/home/dot_local/bin/executable_quick-memo.tmpl new file mode 100644 index 0000000..aad22af --- /dev/null +++ b/home/dot_local/bin/executable_quick-memo.tmpl @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +"""Quick memo - text input or clipboard capture to jax-bot. + +Hotkeys: + F12 - Text input mode (wofi dialog) + SHIFT+F12 - Clipboard mode (text/URL/image) + +Usage: + quick-memo --input # Text input via wofi + quick-memo --clipboard # Capture clipboard content +""" + +import argparse +import subprocess +import sys + +# httpx is optional - fall back to urllib if not available +try: + import httpx + HAS_HTTPX = True +except ImportError: + import urllib.request + import urllib.error + import json as json_module + HAS_HTTPX = False + +# Configuration - chezmoi template with default fallback +{{- $jaxUrl := "http://127.0.0.1:8080" -}} +{{- if hasKey . "jaxBotUrl" -}} +{{- $jaxUrl = .jaxBotUrl -}} +{{- end }} +JAX_URL = "{{ $jaxUrl }}" +NOTIFY_ID = "91192" + + +def notify(title: str, msg: str, critical: bool = False): + """Send desktop notification.""" + cmd = ["notify-send", title, msg, "-r", NOTIFY_ID, "-t", "3000"] + if critical: + cmd.extend(["-u", "critical"]) + subprocess.run(cmd, capture_output=True) + + +def check_jax() -> bool: + """Check if jax-bot is running.""" + try: + if HAS_HTTPX: + httpx.get(f"{JAX_URL}/", timeout=2) + else: + urllib.request.urlopen(f"{JAX_URL}/", timeout=2) + return True + except Exception: + return False + + +def get_text_input() -> str | None: + """Open yad text area dialog.""" + result = subprocess.run( + [ + "yad", "--form", + "--field", ":TXT", + "--title", "Quick Memo", + "--width", "500", + "--height", "300", + "--center", + "--on-top", + "--skip-taskbar", + "--undecorated", + "--borders", "20", + ], + capture_output=True, + text=True, + ) + if result.returncode != 0: + return None # User cancelled + # yad returns field value followed by | + text = result.stdout.strip() + if text.endswith("|"): + text = text[:-1] + return text.strip() + + +def get_clipboard_type() -> str: + """Detect clipboard content type.""" + result = subprocess.run( + ["wl-paste", "--list-types"], + capture_output=True, + text=True, + ) + types = result.stdout.lower() + if "image/png" in types: + return "image/png" + elif "image/jpeg" in types: + return "image/jpeg" + elif "text/plain" in types: + return "text" + return "unknown" + + +def get_clipboard_text() -> str: + """Get text from clipboard.""" + result = subprocess.run(["wl-paste"], capture_output=True, text=True) + return result.stdout.strip() + + +def get_clipboard_image(mime: str) -> bytes: + """Get image bytes from clipboard.""" + result = subprocess.run( + ["wl-paste", "--type", mime], + capture_output=True, + ) + return result.stdout + + +def send_memo_httpx( + content: str | None = None, + image_data: bytes | None = None, + image_type: str | None = None, +) -> dict: + """Send memo via httpx.""" + try: + if image_data: + ext = "png" if "png" in (image_type or "") else "jpg" + response = httpx.post( + f"{JAX_URL}/api/memo", + files={"file": (f"clipboard.{ext}", image_data, image_type)}, + data={"auto_tag": "true"}, + timeout=30, + ) + else: + is_url = (content or "").startswith(("http://", "https://")) + response = httpx.post( + f"{JAX_URL}/api/memo", + data={ + "content": content, + "auto_tag": "true", + "summarize_url": "true" if is_url else "false", + }, + timeout=30, + ) + return response.json() + except Exception as e: + return {"success": False, "error": str(e)} + + +def send_memo_urllib( + content: str | None = None, + image_data: bytes | None = None, + image_type: str | None = None, +) -> dict: + """Send memo via urllib (fallback when httpx unavailable).""" + import io + import uuid + + try: + boundary = uuid.uuid4().hex + + if image_data: + ext = "png" if "png" in (image_type or "") else "jpg" + filename = f"clipboard.{ext}" + + body = io.BytesIO() + body.write(f"--{boundary}\r\n".encode()) + body.write(f'Content-Disposition: form-data; name="file"; filename="{filename}"\r\n'.encode()) + body.write(f"Content-Type: {image_type}\r\n\r\n".encode()) + body.write(image_data) + body.write(f"\r\n--{boundary}\r\n".encode()) + body.write(b'Content-Disposition: form-data; name="auto_tag"\r\n\r\ntrue') + body.write(f"\r\n--{boundary}--\r\n".encode()) + + req = urllib.request.Request( + f"{JAX_URL}/api/memo", + data=body.getvalue(), + headers={"Content-Type": f"multipart/form-data; boundary={boundary}"}, + ) + else: + is_url = (content or "").startswith(("http://", "https://")) + data = urllib.parse.urlencode({ + "content": content or "", + "auto_tag": "true", + "summarize_url": "true" if is_url else "false", + }).encode() + req = urllib.request.Request(f"{JAX_URL}/api/memo", data=data) + + with urllib.request.urlopen(req, timeout=30) as response: + return json_module.loads(response.read().decode()) + except Exception as e: + return {"success": False, "error": str(e)} + + +def send_memo( + content: str | None = None, + image_data: bytes | None = None, + image_type: str | None = None, +) -> dict: + """Send memo to jax-bot API.""" + if HAS_HTTPX: + return send_memo_httpx(content, image_data, image_type) + return send_memo_urllib(content, image_data, image_type) + + +def mode_input(): + """F12: Text input mode via wofi dialog.""" + text = get_text_input() + if not text: + return # User cancelled, no notification + + notify("Quick Memo", "Processing...") + result = send_memo(content=text) + + if result.get("success"): + tags = " ".join(f"#{t}" for t in result.get("tags", [])) + preview = text[:60] + ("..." if len(text) > 60 else "") + notify("Quick Memo", f"Saved!\n{preview}\n{tags}") + else: + notify("Quick Memo", f"Error: {result.get('error')}", critical=True) + + +def mode_clipboard(): + """SHIFT+F12: Clipboard capture mode.""" + clip_type = get_clipboard_type() + + if clip_type == "unknown": + notify("Quick Memo", "Clipboard empty", critical=True) + return + + notify("Quick Memo", "Processing...") + + if clip_type.startswith("image/"): + image_data = get_clipboard_image(clip_type) + if not image_data: + notify("Quick Memo", "Failed to read image", critical=True) + return + result = send_memo(image_data=image_data, image_type=clip_type) + else: + text = get_clipboard_text() + if not text: + notify("Quick Memo", "Clipboard empty", critical=True) + return + result = send_memo(content=text) + + if result.get("success"): + tags = " ".join(f"#{t}" for t in result.get("tags", [])) + preview = result.get("content", "")[:60] + if len(result.get("content", "")) > 60: + preview += "..." + notify("Quick Memo", f"Saved!\n{preview}\n{tags}") + else: + notify("Quick Memo", f"Error: {result.get('error')}", critical=True) + + +def main(): + parser = argparse.ArgumentParser( + description="Quick memo to jax-bot", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=__doc__, + ) + parser.add_argument( + "--input", "-i", + action="store_true", + help="Text input mode (wofi dialog)", + ) + parser.add_argument( + "--clipboard", "-c", + action="store_true", + help="Clipboard capture mode", + ) + args = parser.parse_args() + + if not args.input and not args.clipboard: + parser.print_help() + sys.exit(1) + + if not check_jax(): + notify("Quick Memo", "jax-bot not running", critical=True) + sys.exit(1) + + if args.input: + mode_input() + elif args.clipboard: + mode_clipboard() + + +if __name__ == "__main__": + main() diff --git a/home/dot_tmux.conf b/home/dot_tmux.conf new file mode 100644 index 0000000..4f79906 --- /dev/null +++ b/home/dot_tmux.conf @@ -0,0 +1,104 @@ +# Modern tmux config with sensible defaults + +# --- General --- +set -g default-terminal "tmux-256color" +set -g history-limit 1000000 +set -g mouse on +set -g focus-events on + +# Faster escape time (for vim/TUI apps) +set -sg escape-time 0 +set -sg repeat-time 300 + +# --- Terminal features for modern TUI apps --- +# True color support +set -sa terminal-features ',xterm-ghostty:RGB' +set -sa terminal-features ',xterm-256color:RGB' +set -sa terminal-overrides ',xterm-256color:Tc' + +# Extended keys (Ctrl+Shift, etc. for Claude Code, neovim, etc.) +set -g extended-keys on +set -sa terminal-features ',xterm-ghostty:extkeys' +set -sa terminal-features ',xterm-256color:extkeys' + +# Clipboard via OSC 52 (works over SSH) +set -g set-clipboard on +set -g allow-passthrough on +set -sa terminal-features ',xterm-ghostty:clipboard' +set -sa terminal-features ',xterm-256color:clipboard' + +# Cursor style passthrough (for neovim cursor changes) +set -sa terminal-overrides '*:Ss=\E[%p1%d q:Se=\E[ q' + +# Undercurl support (for neovim diagnostics) +set -sa terminal-overrides ',*:Smulx=\E[4::%p1%dm' +set -sa terminal-overrides ',*:Setulc=\E[58::2::%p1%{65536}%/%d::%p1%{256}%/%{255}%&%d::%p1%{255}%&%d%;m' + +# Start windows and panes at 1 +set -g base-index 1 +setw -g pane-base-index 1 +set -g renumber-windows on + +# --- Prefix --- +unbind C-b +set -g prefix C-a +bind C-a send-prefix + +# --- Keybindings --- +# Reload config +bind r source-file ~/.tmux.conf \; display "Config reloaded" + +# Split panes (intuitive keys) +bind | split-window -h -c "#{pane_current_path}" +bind - split-window -v -c "#{pane_current_path}" +bind c new-window -c "#{pane_current_path}" +unbind '"' +unbind % + +# Vim-style pane navigation +bind h select-pane -L +bind j select-pane -D +bind k select-pane -U +bind l select-pane -R + +# Pane resizing +bind -r H resize-pane -L 5 +bind -r J resize-pane -D 5 +bind -r K resize-pane -U 5 +bind -r L resize-pane -R 5 + +# Window navigation +bind -r n next-window +bind -r p previous-window +bind Tab last-window + +# Copy mode (vi style) +setw -g mode-keys vi +bind -T copy-mode-vi v send -X begin-selection +bind -T copy-mode-vi y send -X copy-selection-and-cancel +bind -T copy-mode-vi Escape send -X cancel + +# --- Status bar --- +set -g status-position top +set -g status-interval 5 +set -g status-style 'bg=#1a1a1a fg=#808080' + +# Left: session name +set -g status-left-length 30 +set -g status-left '#[fg=#c0c0c0,bold] #S #[fg=#404040]|' + +# Right: minimal info +set -g status-right-length 50 +set -g status-right '#[fg=#606060]%H:%M #[fg=#404040]| #[fg=#808080]%d %b' + +# Window status +setw -g window-status-format ' #I:#W ' +setw -g window-status-current-format '#[fg=#c0c0c0,bold] #I:#W ' +setw -g window-status-separator '' + +# Pane borders +set -g pane-border-style 'fg=#303030' +set -g pane-active-border-style 'fg=#606060' + +# Messages +set -g message-style 'bg=#1a1a1a fg=#c0c0c0' diff --git a/home/private_dot_config/chromium-flags.conf b/home/private_dot_config/chromium-flags.conf new file mode 100644 index 0000000..7b64c47 --- /dev/null +++ b/home/private_dot_config/chromium-flags.conf @@ -0,0 +1,3 @@ +--ozone-platform-hint=auto +--enable-wayland-ime +--wayland-text-input-version=3 diff --git a/home/private_dot_config/fish/config.fish b/home/private_dot_config/fish/config.fish new file mode 100644 index 0000000..a7e2b53 --- /dev/null +++ b/home/private_dot_config/fish/config.fish @@ -0,0 +1,50 @@ +# Man pages with bat +set -x MANROFFOPT "-c" +set -x MANPAGER "sh -c 'col -bx | bat -l man -p'" + +# ~/.local/bin +fish_add_path -m $HOME/.local/bin + +# LM Studio +fish_add_path $HOME/.lmstudio/bin + +# Scaleway (lazy-load) +function scw --wraps=scw + functions -e scw + eval (command scw autocomplete script shell=fish) + command scw $argv +end + +# Kubernetes +set -e SHOW_K8S +kubeswitch kubectl-alias kubectl +kubeswitch inherit-env +set -gx KUBECONFIG /dev/null + +# History with timestamps +function history + builtin history --show-time='%F %T ' +end + +# Bash-style !! and !$ +function __history_previous_command + switch (commandline -t) + case "!" + commandline -t $history[1]; commandline -f repaint + case "*" + commandline -i ! + end +end + +function __history_previous_command_arguments + switch (commandline -t) + case "!" + commandline -t "" + commandline -f history-token-search-backward + case "*" + commandline -i '$' + end +end + +bind ! __history_previous_command +bind '$' __history_previous_command_arguments diff --git a/home/private_dot_config/fish/fish_plugins b/home/private_dot_config/fish/fish_plugins new file mode 100644 index 0000000..a619d61 --- /dev/null +++ b/home/private_dot_config/fish/fish_plugins @@ -0,0 +1,6 @@ +fabioantunes/fish-nvm +edc/bass +jorgebucaran/fisher +jethrokuan/z +eth-p/fish-kubeswitch +franciscolourenco/done diff --git a/home/private_dot_config/fish/functions/fish_prompt.fish b/home/private_dot_config/fish/functions/fish_prompt.fish new file mode 100644 index 0000000..db094a1 --- /dev/null +++ b/home/private_dot_config/fish/functions/fish_prompt.fish @@ -0,0 +1,22 @@ +function fish_prompt + set -l last_status $status + + # Only if the user has explicitly flipped SHOW_K8S on do we fetch & show + if test "$SHOW_K8S" = "1" + # Single kubectl call for both context and namespace + set -l kube_info (kubectl config view --minify --output 'jsonpath={.current-context}/{..namespace}' 2>/dev/null) + if test -n "$kube_info" + # Default namespace if none set + set kube_info (string replace -r '/$' '/default' $kube_info) + echo -n "[k8s:$kube_info] " + end + end + + # Red prompt on error, normal otherwise + if test $last_status -ne 0 + set_color red + end + printf '%s> ' (prompt_pwd) + set_color normal +end + diff --git a/home/private_dot_config/fish/functions/gha-cancel-run.fish b/home/private_dot_config/fish/functions/gha-cancel-run.fish new file mode 100644 index 0000000..8400501 --- /dev/null +++ b/home/private_dot_config/fish/functions/gha-cancel-run.fish @@ -0,0 +1,36 @@ +function gha-cancel-run --description "Cancel a GitHub Actions run using its URL" + if test -z "$argv[1]" + echo "Usage: gha-cancel-run " + echo "Example: gha-cancel-run https://github.com/OWNER/REPO/actions/runs/123456789" + return 1 + end + + set -l run_url $argv[1] + + # Extract owner, repo, and run_id from the URL using regular expressions + # This regex is specifically designed to parse the run URL format + if not string match --regex 'https://github.com/([^/]+)/([^/]+)/actions/runs/([0-9]+)' "$run_url" + echo "Error: Invalid GitHub Actions run URL format." + echo "Expected format: https://github.com/OWNER/REPO/actions/runs/RUN_ID" + return 1 + end + + set -l owner (string match --regex --groups=1 'https://github.com/([^/]+)/([^/]+)/actions/runs/([0-9]+)' "$run_url")[2] + set -l repo (string match --regex --groups=2 'https://github.com/([^/]+)/([^/]+)/actions/runs/([0-9]+)' "$run_url")[2] + set -l run_id (string match --regex --groups=3 'https://github.com/([^/]+)/([^/]+)/actions/runs/([0-9]+)' "$run_url")[2] + + echo "Attempting to cancel run ID: $run_id in $owner/$repo..." + + gh api \ + --method POST \ + -H "Accept: application/vnd.github.v3+json" \ + "/repos/$owner/$repo/actions/runs/$run_id/cancel" + + if test $status -eq 0 + echo "Cancellation request sent successfully for run ID: $run_id" + else + echo "Failed to send cancellation request for run ID: $run_id" + echo "Check the error message above for details." + return 1 + end +end diff --git a/home/private_dot_config/fish/functions/j.fish b/home/private_dot_config/fish/functions/j.fish new file mode 100644 index 0000000..ce8c5bd --- /dev/null +++ b/home/private_dot_config/fish/functions/j.fish @@ -0,0 +1,3 @@ +function j --wraps=z + z $argv +end diff --git a/home/private_dot_config/fish/functions/koff.fish b/home/private_dot_config/fish/functions/koff.fish new file mode 100644 index 0000000..289996d --- /dev/null +++ b/home/private_dot_config/fish/functions/koff.fish @@ -0,0 +1,3 @@ +function koff --description "Disable k8s prompt" + set -e SHOW_K8S +end diff --git a/home/private_dot_config/fish/functions/ktx.fish b/home/private_dot_config/fish/functions/ktx.fish new file mode 100644 index 0000000..aaa0dba --- /dev/null +++ b/home/private_dot_config/fish/functions/ktx.fish @@ -0,0 +1,4 @@ +function ktx --description "Switch k8s context and enable prompt" + kubeswitch ctx $argv + set -g SHOW_K8S 1 +end diff --git a/home/private_dot_config/fish/functions/tmpdir.fish b/home/private_dot_config/fish/functions/tmpdir.fish new file mode 100644 index 0000000..04c75bc --- /dev/null +++ b/home/private_dot_config/fish/functions/tmpdir.fish @@ -0,0 +1,11 @@ +function tmpdir --description "Create and cd into a temp directory under ~/tmp" + set -l date (date +%Y%m%d) + + if test -n "$argv[1]" + set -l dir ~/tmp/$date-$argv[1] + mkdir -p $dir && cd $dir + else + set -l dir ~/tmp/{$date}_(random) + mkdir -p $dir && cd $dir + end +end diff --git a/home/private_dot_config/fish/functions/tt.fish b/home/private_dot_config/fish/functions/tt.fish new file mode 100644 index 0000000..8ba2d36 --- /dev/null +++ b/home/private_dot_config/fish/functions/tt.fish @@ -0,0 +1,3 @@ +function tt --wraps=tmpdir + tmpdir $argv +end diff --git a/home/private_dot_config/ghostty/config b/home/private_dot_config/ghostty/config new file mode 100644 index 0000000..76eadbc --- /dev/null +++ b/home/private_dot_config/ghostty/config @@ -0,0 +1,50 @@ +# Ghostty Configuration +# https://ghostty.org/docs/config + +# Appearance +background = #141414 +background-opacity = 0.9 +background-blur-radius = 20 + +# Window +window-padding-x = 12 +window-padding-y = 12 +window-padding-balance = true +window-decoration = server + +# Font +font-size = 12 +font-thicken = true + +# Cursor +cursor-style = block +cursor-style-blink = false + +# Scrollback +scrollback-limit = 4294967295 + +# Clipboard (OSC 52 for neovim/tmux/SSH) +copy-on-select = true +clipboard-trim-trailing-spaces = true +clipboard-read = allow +clipboard-write = allow + +# Shell integration with SSH support +shell-integration = detect +shell-integration-features = cursor,sudo,title,ssh-env,ssh-terminfo + +# Mouse +mouse-hide-while-typing = true + +# Misc +confirm-close-surface = false + +# Hyprland/Wayland +gtk-single-instance = true +class = ghostty + +# Quick terminal (quake-style dropdown) +quick-terminal-position = top +quick-terminal-animation-duration = 0.1 +quick-terminal-autohide = true +keybind = global:ctrl+grave_accent=toggle_quick_terminal diff --git a/home/private_dot_config/hypr/autostart.conf.tmpl b/home/private_dot_config/hypr/autostart.conf.tmpl index 7a42872..afab9a5 100644 --- a/home/private_dot_config/hypr/autostart.conf.tmpl +++ b/home/private_dot_config/hypr/autostart.conf.tmpl @@ -4,6 +4,9 @@ # Polkit agent (GUI password prompts) exec-once = /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 +# XWayland support +exec-once = xwayland-satellite + # System tray & bar exec-once = nm-applet & exec-once = waybar diff --git a/home/private_dot_config/hypr/hyprland.conf.tmpl b/home/private_dot_config/hypr/hyprland.conf.tmpl index 1a08992..4ae22e7 100644 --- a/home/private_dot_config/hypr/hyprland.conf.tmpl +++ b/home/private_dot_config/hypr/hyprland.conf.tmpl @@ -102,6 +102,11 @@ windowrulev2 = rounding 0, floating:0, onworkspace:w[tv1] windowrulev2 = bordersize 0, floating:0, onworkspace:f[1] windowrulev2 = rounding 0, floating:0, onworkspace:f[1] +# Quick Memo popup (yad) +windowrulev2 = float, class:^(yad)$ +windowrulev2 = center, class:^(yad)$ +windowrulev2 = pin, class:^(yad)$ + binds { hide_special_on_workspace_change = true workspace_back_and_forth = true @@ -212,9 +217,12 @@ bind = $mainMod, 9, workspace, 9 # Special workspaces bind = SUPER, F12, exec, ~/.local/bin/workspace-pin 1337 -bind = , F12, togglespecialworkspace, org bind = SUPER, A, togglespecialworkspace, org -bind = SUPER SHIFT, F12, movetoworkspace, special:org +bind = SUPER SHIFT, A, movetoworkspace, special:org + +# Quick Memo (jax-bot integration) +bind = , F12, exec, ~/.local/bin/quick-memo --input +bind = SHIFT, F12, exec, ~/.local/bin/quick-memo --clipboard bind = SUPER, X, workspace, name:media bind = SUPER SHIFT, X, movetoworkspace, name:media bind = SUPER SHIFT, C, movetoworkspace, name:tg