htoolbox 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,245 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ # Usage:
5
+ # host="100.123.68.125"
6
+ # remote_addr="127.0.0.1" # or "0.0.0.0" or any ip bound on remote
7
+ # port=8000
8
+ # kill_remote_vscode "$host" "$remote_addr" "$port"
9
+ #
10
+ # Returns:
11
+ # 0 = killed successfully
12
+ # 1 = nothing to kill / no listener found
13
+ # 2 = listener found but not Code/code-tunnel (skipped)
14
+ # 3 = error (ssh failure / parsing failure)
15
+ kill_remote_vscode_windows() {
16
+ local HOST="$1" REMOTE_ADDR="$2" PORT="$3"
17
+ local netstat_cmd="netstat -ano | findstr \"${REMOTE_ADDR}:${PORT}\" | findstr LISTENING || true"
18
+ local netline
19
+ netline=$(ssh "$HOST" "$netstat_cmd") || { echo "ERROR: ssh netstat failed" >&2; return 3; }
20
+ if [[ -z "$netline" ]]; then
21
+ echo "No listener found on ${REMOTE_ADDR}:${PORT} on ${HOST}."; return 1; fi
22
+ local pid
23
+ pid=$(printf '%s\n' "$netline" | awk '{print $NF}' | head -n1)
24
+ if [[ -z "$pid" ]]; then
25
+ echo "Failed to parse PID from netstat output:" >&2; printf '%s\n' "$netline" >&2; return 3; fi
26
+ local taskline tasklist_cmd="tasklist /FI \"PID eq ${pid}\" /FO CSV /NH"
27
+ taskline=$(ssh "$HOST" "$tasklist_cmd") || { echo "ERROR: ssh tasklist failed" >&2; return 3; }
28
+ if printf '%s\n' "$taskline" | grep -qi "No tasks are running"; then echo "PID $pid not present."; return 1; fi
29
+ local procname lower
30
+ procname=$(printf '%s\n' "$taskline" | awk -F',' '{gsub(/"/,"",$1); print $1}' | head -n1)
31
+ lower=$(printf '%s' "$procname" | tr '[:upper:]' '[:lower:]')
32
+ if [[ "$lower" == "code.exe" || "$lower" == "code" || "$lower" == "code-tunnel.exe" || "$lower" == "code-tunnel" ]]; then
33
+ echo "Killing windows PID $pid on $HOST ..."
34
+ ssh "$HOST" "taskkill /PID ${pid} /F" && { echo "Killed PID $pid."; return 0; }
35
+ echo "Failed to kill PID $pid." >&2; return 3
36
+ else
37
+ echo "PID $pid belongs to '$procname' — not Code/code-tunnel. Skipping kill."; return 2
38
+ fi
39
+ }
40
+
41
+ kill_remote_vscode_unix() {
42
+ # Linux / macOS: find PIDs listening on REMOTE_ADDR:PORT, prefer lsof then ss, validate by command line, then kill
43
+ local HOST="$1" REMOTE_ADDR="$2" PORT="$3"
44
+ # Build remote script safely using a heredoc with placeholder substitution to avoid local shell quoting issues
45
+ local REMOTE_SCRIPT
46
+ REMOTE_SCRIPT=$(cat <<'EOS'
47
+ set -euo pipefail
48
+ found_pids=""
49
+ if command -v lsof >/dev/null 2>&1; then
50
+ found_pids=$(lsof -nP -iTCP:__PORT__ -sTCP:LISTEN -t 2>/dev/null || true)
51
+ fi
52
+ if [ -z "$found_pids" ] && command -v ss >/dev/null 2>&1; then
53
+ found_pids=$(ss -ltnp 2>/dev/null | awk -v port=__PORT__ '$4 ~ ":"port"$" {print}' | sed -n 's/.*pid=\([0-9]\+\).*/\1/p')
54
+ fi
55
+ if [ -z "$found_pids" ] && command -v pgrep >/dev/null 2>&1; then
56
+ found_pids=$(pgrep -f "serve-web.*(--port[= ]__PORT__|:__PORT__)" || true)
57
+ fi
58
+ killed_any=1
59
+ if [ -n "$found_pids" ]; then
60
+ for pid in $found_pids; do
61
+ cmdline=$(ps -p "$pid" -o args= 2>/dev/null || true)
62
+ echo "$cmdline" | grep -qiE "(code(|-tunnel).*(serve-web)|serve-web)" || continue
63
+ echo "Killing PID $pid ($cmdline)"
64
+ kill -TERM "$pid" 2>/dev/null || true
65
+ done
66
+ sleep 0.5
67
+ for pid in $found_pids; do
68
+ if kill -0 "$pid" 2>/dev/null; then
69
+ kill -KILL "$pid" 2>/dev/null || true
70
+ fi
71
+ done
72
+ killed_any=0
73
+ fi
74
+ exit "$killed_any"
75
+ EOS
76
+ )
77
+ REMOTE_SCRIPT=${REMOTE_SCRIPT//__PORT__/$PORT}
78
+
79
+ if ssh "$HOST" bash -lc "$REMOTE_SCRIPT"; then
80
+ echo "Remote VS Code server cleaned up (Linux/macOS)."; return 0
81
+ else
82
+ echo "No matching VS Code server found to clean on $HOST (Linux/macOS)."; return 1
83
+ fi
84
+ }
85
+
86
+
87
+ # ----------------------------------
88
+ # Defaults
89
+ # ----------------------------------
90
+ PORT=8000
91
+ REMOTE_ADDR="127.0.0.1"
92
+ REMOTE_PORT=8000
93
+ BIND_HOST_IPV4=false
94
+ KILL_MODE=false
95
+
96
+ # ----------------------------------
97
+ # Usage
98
+ # ----------------------------------
99
+ usage() {
100
+ cat <<EOF
101
+ Usage: $0 [options] <host>
102
+
103
+ Options:
104
+ -p, --port <port> Local/forward port (default: 8000)
105
+ -r, --remote <addr[:p]> Remote bind address and optional port (default: 127.0.0.1)
106
+ -b, --bindhost Use detected IPv4 of <host> as remote bind address
107
+ (mutually exclusive with --remote)
108
+ -k, --kill Kill the VS Code server on the given port/host
109
+ -h, --help Show this help
110
+
111
+ Examples:
112
+ $0 myhost
113
+ $0 -p 8080 myhost
114
+ $0 -r 0.0.0.0 myhost
115
+ $0 -r 192.168.1.10:9000 myhost
116
+ $0 --kill -r 0.0.0.0:9000 myhost
117
+ $0 -b myhost # bind the VS Code server to the host's IPv4
118
+ EOF
119
+ exit 1
120
+ }
121
+
122
+ # ----------------------------------
123
+ # Parse args
124
+ # ----------------------------------
125
+ REMOTE_SPEC=""
126
+ while [[ $# -gt 0 ]]; do
127
+ case "$1" in
128
+ -p|--port)
129
+ shift; PORT="${1:-}"; [[ -z "$PORT" ]] && usage ;;
130
+ -r|--remote)
131
+ shift; REMOTE_SPEC="${1:-}"; [[ -z "$REMOTE_SPEC" ]] && usage ;;
132
+ -b|--bindhost)
133
+ BIND_HOST_IPV4=true ;;
134
+ -k|--kill)
135
+ KILL_MODE=true ;;
136
+ -h|--help)
137
+ usage ;;
138
+ -*)
139
+ echo "Unknown option: $1" >&2; usage ;;
140
+ *)
141
+ HOST="$1" ;;
142
+ esac
143
+ shift
144
+ done
145
+
146
+ [[ -z "${HOST:-}" ]] && usage
147
+
148
+ # ----------------------------------
149
+ # Validate mutual exclusivity
150
+ # ----------------------------------
151
+ if [[ "$BIND_HOST_IPV4" == true && -n "$REMOTE_SPEC" ]]; then
152
+ echo "Error: --bindhost (-b) cannot be used together with --remote (-r)." >&2
153
+ exit 1
154
+ fi
155
+
156
+ # ----------------------------------
157
+ # Resolve host IPv4
158
+ # ----------------------------------
159
+ IPV4=$(ping -c 1 "$HOST" 2>/dev/null | grep -E -o "\(.*\):" | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+") || true
160
+ if [[ -n "$IPV4" ]]; then
161
+ echo "Host $HOST is mapped to $IPV4"
162
+ else
163
+ IPV4="127.0.0.1"
164
+ echo "Failed to resolve IPv4 for $HOST; using 127.0.0.1"
165
+ fi
166
+
167
+ # ----------------------------------
168
+ # Parse remote spec (ip[:port])
169
+ # ----------------------------------
170
+ if [[ -n "$REMOTE_SPEC" ]]; then
171
+ if [[ "$REMOTE_SPEC" == *:* ]]; then
172
+ REMOTE_ADDR="${REMOTE_SPEC%%:*}"
173
+ REMOTE_PORT="${REMOTE_SPEC##*:}"
174
+ elif [[ "$REMOTE_SPEC" =~ ^[0-9]+$ ]]; then
175
+ REMOTE_PORT="$REMOTE_SPEC"
176
+ else
177
+ REMOTE_ADDR="$REMOTE_SPEC"
178
+ fi
179
+ fi
180
+
181
+ # If user wants to bind to detected IPv4
182
+ if [[ "$BIND_HOST_IPV4" == true ]]; then
183
+ REMOTE_ADDR="$IPV4"
184
+ fi
185
+
186
+ # ----------------------------------
187
+ # Detect remote OS
188
+ # ----------------------------------
189
+ echo "Detecting OS on $HOST ..."
190
+ REMOTE_OS=$(ssh -o BatchMode=yes -o ConnectTimeout=5 "$HOST" 'uname' | head -n 1)
191
+
192
+ case "$REMOTE_OS" in
193
+ *[Ll]inux*)
194
+ OS_TYPE="linux"
195
+ CODE_CMD="code"
196
+ ;;
197
+ *[Dd]arwin*)
198
+ OS_TYPE="mac"
199
+ CODE_CMD="/Applications/Visual\\ Studio\\ Code.app/Contents/Resources/app/bin/code"
200
+ ;;
201
+ *[Mm][Ss][Yy][Ss]* | *[Mm][Ii][Nn][Gg][Ww]* | *[Cc][Yy][Gg][Ww]* | *[Ww]indows*)
202
+ OS_TYPE="windows"
203
+ CODE_CMD="\"d:/App/Microsoft VS Code/bin/code\""
204
+ ;;
205
+ *)
206
+ OS_TYPE="unknown"
207
+ CODE_CMD="code"
208
+ ;;
209
+ esac
210
+
211
+ echo "Detected remote OS: $OS_TYPE ($REMOTE_OS)"
212
+
213
+ cleanup_remote() {
214
+ echo "Cleaning up remote VS Code server on $HOST (host: $REMOTE_ADDR, port: $REMOTE_PORT)..."
215
+ case "$OS_TYPE" in
216
+ windows)
217
+ kill_remote_vscode_windows "$HOST" "$REMOTE_ADDR" "$REMOTE_PORT" || true ;;
218
+ linux|mac)
219
+ kill_remote_vscode_unix "$HOST" "$REMOTE_ADDR" "$REMOTE_PORT" || true ;;
220
+ *)
221
+ echo "Unknown remote OS; attempted generic cleanup..." ;;
222
+ esac
223
+ }
224
+
225
+ # Ensure we attempt cleanup on Ctrl+C or script exit
226
+ trap cleanup_remote INT TERM EXIT
227
+
228
+ # ----------------------------------
229
+ # Kill mode (explicit command)
230
+ # ----------------------------------
231
+ if [[ "$KILL_MODE" == true ]]; then
232
+ cleanup_remote
233
+ exit 0
234
+ fi
235
+
236
+
237
+ echo -e "\n------------------------------------------------------------------\n"
238
+
239
+
240
+ # ----------------------------------
241
+ # Start VS Code server
242
+ # ----------------------------------
243
+ echo "Starting VS Code server on $HOST (bind ${REMOTE_ADDR}:${REMOTE_PORT})..."
244
+ ssh -L "localhost:${PORT}:${REMOTE_ADDR}:${REMOTE_PORT}" "$HOST" \
245
+ "${CODE_CMD} serve-web --without-connection-token --accept-server-license-terms --host ${REMOTE_ADDR} --port ${REMOTE_PORT}"
@@ -0,0 +1,44 @@
1
+ Metadata-Version: 2.4
2
+ Name: htoolbox
3
+ Version: 0.1.0
4
+ Summary: This package includes various tools for developers.
5
+ Requires-Python: >=3.12
6
+ Description-Content-Type: text/markdown
7
+ License-File: LICENSE
8
+ Dynamic: license-file
9
+
10
+ # HToolbox
11
+
12
+ This repository provides some useful tools that fit my developing habits.
13
+
14
+ ## Install
15
+
16
+ To install locally (editable mode) and get the `tmuxer` command:
17
+
18
+ ```bash
19
+ pip install -e .
20
+ ```
21
+
22
+ Or to install from source (non-editable):
23
+
24
+ ```bash
25
+ pip install .
26
+ ```
27
+
28
+ ## tmuxer
29
+
30
+ Run the installed command:
31
+
32
+ ```bash
33
+ tmuxer -s mysession -n 3 --layout ev
34
+ ```
35
+
36
+ This will start (or attach) to a tmux session named `mysession` with 3 panes using the `even-vertical` layout (`ev`).
37
+
38
+ ## codeit
39
+
40
+ This tool is a quick script for starting vscode server on a remote server, and utilizing SSH port forwarding to access it locally.
41
+
42
+ ```bash
43
+ codeit <remote-host>
44
+ ```
@@ -0,0 +1,8 @@
1
+ tmuxer.py,sha256=ZM_eKNb0XAY5rgsSX_hs6f9OrDFqf3q39WaN0IqvIZk,3090
2
+ htoolbox-0.1.0.data/scripts/codeit,sha256=hC5Jib7Ka8h5rgRnM4Jy-QFJLdrgVgKiqgFCdunk6oE,7904
3
+ htoolbox-0.1.0.dist-info/licenses/LICENSE,sha256=518WNThsP4IMvqMZ4PM-gXwqPvBwBlF4LvUmAJjVdDM,1070
4
+ htoolbox-0.1.0.dist-info/METADATA,sha256=7x7hq-aY88U9YaqWz1wo87cBZ7TuPhj0ZApJlT1Ez6o,871
5
+ htoolbox-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
6
+ htoolbox-0.1.0.dist-info/entry_points.txt,sha256=Lme2V1g3rRAq8tyxNmu70eS80h-LBeP03giTfP8E4fA,39
7
+ htoolbox-0.1.0.dist-info/top_level.txt,sha256=H5dSXvsTcYsU3PtS2Ya1w-GQct2fWxmbFHJ61GnP92E,7
8
+ htoolbox-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ tmuxer = tmuxer:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Zhanghan Wang
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ tmuxer
tmuxer.py ADDED
@@ -0,0 +1,90 @@
1
+ import os
2
+ import argparse
3
+ import sys
4
+
5
+
6
+ def start_tmux_session(session_name, window_name=None, pane_index=None, new_panes=1, layout="even-vertical"):
7
+ """Start and attach to a tmux session.
8
+
9
+ This function uses the `tmux` command-line tool. It is designed to be
10
+ called from the CLI entry point `main()` below, but can also be imported
11
+ and used programmatically.
12
+ """
13
+ # Start a new tmux session detached
14
+ os.system(f"tmux new-session -d -s {session_name}")
15
+
16
+ # Create a new window if specified
17
+ if window_name:
18
+ os.system(f"tmux new-window -t {session_name} -n {window_name}")
19
+
20
+ # Create new panes if specified
21
+ for i in range(1, new_panes):
22
+ os.system(f"tmux split-window -t {session_name}")
23
+ os.system(f"tmux select-layout -t {session_name} {layout}")
24
+
25
+ # Export IID environment variable into each pane
26
+ for i in range(new_panes):
27
+ os.system(f"tmux send-keys -t {session_name}.{i} 'export IID={i}' C-m")
28
+
29
+ # Select the specified pane if provided
30
+ if pane_index is not None:
31
+ os.system(f"tmux select-pane -t {session_name}:{pane_index}")
32
+
33
+ # Attach to the tmux session
34
+ os.system(f"tmux attach-session -t {session_name}")
35
+
36
+
37
+ def _normalize_layout(layout_arg: str) -> str:
38
+ v = layout_arg.lower().strip()
39
+ if v == "eh":
40
+ return "even-horizontal"
41
+ if v == "ev":
42
+ return "even-vertical"
43
+ if v == "mh":
44
+ return "main-horizontal"
45
+ if v == "mv":
46
+ return "main-vertical"
47
+ if v == "t":
48
+ return "tiled"
49
+ return layout_arg
50
+
51
+
52
+ def main():
53
+ """Console entry point for the `tmuxer` command.
54
+
55
+ Accepts an optional argv list (for testing). Returns 0 on success, may
56
+ raise exceptions for misuse.
57
+ """
58
+ parser = argparse.ArgumentParser(description="Tmux session starter")
59
+ parser.add_argument("-n", "--num_panes", type=int, required=True, help="Number of new panes to create")
60
+ parser.add_argument("-s", "--session", type=str, required=True, help="Name of the tmux session")
61
+ parser.add_argument("-w", "--window", type=str, help="Name of the tmux window")
62
+ parser.add_argument("-p", "--pane", type=int, help="Index of the tmux pane")
63
+ parser.add_argument("--layout", type=str, default="even-vertical", help="Layout for the tmux panes")
64
+ parser.add_argument(
65
+ "--kill", action="store_true", help="Kill existing tmux session with the same name before starting a new one"
66
+ )
67
+
68
+ args = parser.parse_args()
69
+
70
+ if args.kill and args.session:
71
+ # Prefer to silence output from tmux kill-session
72
+ os.system(f"tmux kill-session -t {args.session} >/dev/null 2>&1")
73
+
74
+ if args.num_panes < 1:
75
+ raise ValueError("Number of new panes must be at least 1")
76
+
77
+ layout = _normalize_layout(args.layout)
78
+
79
+ # If session name is not provided, let tmux create a session with default name
80
+ start_tmux_session(
81
+ session_name=args.session,
82
+ window_name=args.window,
83
+ pane_index=args.pane,
84
+ new_panes=args.num_panes,
85
+ layout=layout,
86
+ )
87
+
88
+
89
+ if __name__ == "__main__":
90
+ main()