Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3a1bc94
test(terminal): pin how a real network device renders
gomanizm Aug 26, 2026
0a1e879
feat(terminal): add the stream parser skeleton
gomanizm Aug 26, 2026
83df635
feat(terminal): parse CSI sequences
gomanizm Aug 26, 2026
55208e5
feat(terminal): parse DCS strings, completing the state machine
gomanizm Aug 26, 2026
2256562
feat(terminal): model character attributes (SGR)
gomanizm Aug 26, 2026
c3bc740
feat(terminal): add the screen model core
gomanizm Aug 26, 2026
62cdcc2
feat(terminal): erase, edit and confine - the destructive half of CSI
gomanizm Aug 26, 2026
2d3361f
feat(terminal): alternate screen, modes, charsets and resize
gomanizm Aug 26, 2026
9441c81
feat(terminal): render through the screen model
gomanizm Aug 26, 2026
736b612
refactor(terminal): drop the regex control-code interpreter
gomanizm Aug 26, 2026
4afa010
refactor(terminal): remove the full-screen notice and its lookouts
gomanizm Aug 26, 2026
dd5a331
feat(terminal): paint SGR attributes for real
gomanizm Aug 26, 2026
1e4b1c9
feat(terminal): follow the window size and tell the device
gomanizm Aug 26, 2026
0d053e6
feat(terminal): honour the input modes applications set
gomanizm Aug 26, 2026
8b9861a
test(terminal): pin the ECMA-48 meaning of EL 1
gomanizm Aug 26, 2026
a858645
fix(terminal): stop the screen model losing text and hanging
gomanizm Aug 26, 2026
787d013
fix(terminal): make the screen fill the view, and keep notices straight
gomanizm Aug 26, 2026
c97c4d4
test(connect): stop pinning the line endings of a failure notice
gomanizm Aug 26, 2026
62c7a1f
fix(terminal): remember which line breaks are only wrapping
gomanizm Aug 27, 2026
73e5081
fix(terminal): stop re-splitting lines when the window resizes
gomanizm Aug 27, 2026
c92e02d
feat(ui): show the terminal size in the status bar
gomanizm Aug 27, 2026
b3b7c9d
feat(terminal): let terminal tabs be dragged into a new order
gomanizm Aug 27, 2026
b8409cf
fix(terminal): do not pad a line that wraps into the next one
gomanizm Aug 27, 2026
12a3b2b
feat(ui): let the device list be hidden like the tool area
gomanizm Aug 27, 2026
9c58191
fix(terminal): never index a line by the screen's width
gomanizm Aug 27, 2026
8f1bbd7
feat(update): tell people what changed, not just how to install
gomanizm Aug 27, 2026
8ea0d44
fix(ui): bring back a device list that was dragged to zero width
gomanizm Aug 27, 2026
b204c59
fix(update): render the release notes instead of showing the markup
gomanizm Aug 27, 2026
511b453
ci: verify the release notes before the tag, not after
gomanizm Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# 実機から採った受信バイトは、1 バイトも変えずに持つ。
#
# core.autocrlf が有効な環境では、git が中身を見てテキストと判断した
# ファイルの CRLF を LF へ正規化する。cisco_ios_vt100.bin は制御文字が
# CR/LF/BS しか無く平文に近いため、これに巻き込まれて CR が 147 個
# すべて剥がされていた。作業ツリーでは元へ戻るので気づけず、
# Linux での clone や、リリースに添付される Source code アーカイブ
# (GPL の対応ソース)だけが壊れた状態で配られていた。
tests/fixtures/*.bin -text
49 changes: 49 additions & 0 deletions .github/release-body.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
## NetBelt ポータブル版

### 📄 ライセンスと対応するソースコード

NetBelt は GNU General Public License v3.0 で配布しています。

この実行ファイルには PyQt6 (GPL-3.0)、Qt (LGPL-3.0)、paramiko (LGPL-2.1-or-later)
などが組み込まれています。**この実行ファイルに対応するソースコード**は、
本リリースに自動添付される Source code アーカイブ、および
https://github.com/gomanizm/NetBelt のタグ `{{TAG}}` で入手できます。
ビルドは同梱の `requirements.txt`(バージョン固定済み)と `NetBelt.spec`、
Python 3.11 で再現できます。

組み込まれた第三者パッケージの一覧・ライセンス本文・配布元は、
ZIP 内の `THIRD-PARTY-NOTICES.txt` を参照してください。

### 📦 パッケージ内容
- `NetBelt.exe` - アプリケーション本体
- `README.txt` - 使い方ガイド
- `LICENSE.txt` - NetBelt のライセンス(GPL-3.0)
- `THIRD-PARTY-NOTICES.txt` - 同梱した第三者パッケージのライセンス表示
- `CHANGELOG.txt` - この版で変わったこと
- `.sha256` - ZIP のチェックサム。自動更新がダウンロード後に照合し、
一致しなければ更新を中止します(破損の検知が目的で、GitHub 自体が
侵害された場合の改ざんは検知できません)
- `mibs/` - MIB ファイルの配置先(既定では空。README を参照)

### 🚀 インストール方法
1. ZIPファイルをダウンロード
2. 任意のフォルダに展開
3. `NetBelt.exe` をダブルクリックして起動

### 💻 動作環境
- Windows 10 / Windows 11 (64bit)
- Pythonのインストールは不要
- すべての依存ファイルが含まれています

### ✨ 主な機能
- SSH、Telnet、シリアル接続に対応
- デバイス管理とグループ化
- マクロ機能
- SNMP Trap / Syslog 受信(カスタムMIB対応)
- SFTP ファイル転送

### 📝 初回起動
- 初回起動時に `config.json` が自動生成されます
- MIB ファイルは同梱していません。`mibs/README.md` の手順で配置してください

詳しい使い方は [README.md](https://github.com/gomanizm/NetBelt/blob/main/README.md) をご覧ください。
83 changes: 33 additions & 50 deletions .github/workflows/build-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,63 +101,46 @@ jobs:
name: NetBelt-Portable
path: NetBelt-v*-Windows-Portable.zip

# 更新の通知はリリース本文をそのまま出す。案内文だけだと、利用者は
# 何が変わったのか分からない。CHANGELOG のこの版の節を先頭へ置き、
# 案内文 (.github/release-body.md) をその下へ続ける。
# タグのときだけに絞らない。公開はしないので手動実行でも走らせ、
# 本文の組み立てが壊れていないことをタグを打つ前に確かめられるようにする
- name: Build release notes
shell: pwsh
run: |
$version = '${{ steps.version.outputs.version }}'
$lines = Get-Content CHANGELOG.md -Encoding utf8
$start = $null
$end = $lines.Count
# -like は [ ] を文字クラスとして解釈するので使えない。
# "## [1.2.0]*" は「1 . 2 0 のどれか1文字」という意味になり一致しない
for ($i = 0; $i -lt $lines.Count; $i++) {
if ($lines[$i].StartsWith("## [$version]")) { $start = $i + 1; continue }
if ($null -ne $start -and $lines[$i].StartsWith('## [')) { $end = $i; break }
}
if ($null -eq $start) { throw "CHANGELOG.md に [$version] の節がありません" }
$section = ($lines[$start..($end - 1)] -join "`n").Trim()
if (-not $section) { throw "[$version] の節が空です" }

$template = (Get-Content .github/release-body.md -Encoding utf8) -join "`n"
$template = $template.Replace('{{TAG}}', $env:GITHUB_REF_NAME)

$body = "## v$version で変わったこと`n`n$section`n`n---`n`n$template"
Set-Content -Path release_notes.md -Value $body -Encoding utf8
Write-Host "本文 $($body.Length) 文字 (うち変更点 $($section.Length) 文字)"
Write-Host "--- 先頭 8 行 ---"
Get-Content release_notes.md -Encoding utf8 -TotalCount 8 | Write-Host
if ($body -match '\{\{TAG\}\}') { throw 'テンプレートの置換が残っています' }

- name: Create Release
if: startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v3
with:
files: |
NetBelt-v*-Windows-Portable.zip
NetBelt-v*-Windows-Portable.zip.sha256
body: |
## NetBelt ポータブル版

### 📄 ライセンスと対応するソースコード

NetBelt は GNU General Public License v3.0 で配布しています。

この実行ファイルには PyQt6 (GPL-3.0)、Qt (LGPL-3.0)、paramiko (LGPL-2.1-or-later)
などが組み込まれています。**この実行ファイルに対応するソースコード**は、
本リリースに自動添付される Source code アーカイブ、および
https://github.com/gomanizm/NetBelt のタグ `${{ github.ref_name }}` で入手できます。
ビルドは同梱の `requirements.txt`(バージョン固定済み)と `NetBelt.spec`、
Python 3.11 で再現できます。

組み込まれた第三者パッケージの一覧・ライセンス本文・配布元は、
ZIP 内の `THIRD-PARTY-NOTICES.txt` を参照してください。

### 📦 パッケージ内容
- `NetBelt.exe` - アプリケーション本体
- `README.txt` - 使い方ガイド
- `LICENSE.txt` - NetBelt のライセンス(GPL-3.0)
- `THIRD-PARTY-NOTICES.txt` - 同梱した第三者パッケージのライセンス表示
- `CHANGELOG.txt` - この版で変わったこと
- `.sha256` - ZIP のチェックサム。自動更新がダウンロード後に照合し、
一致しなければ更新を中止します(破損の検知が目的で、GitHub 自体が
侵害された場合の改ざんは検知できません)
- `mibs/` - MIB ファイルの配置先(既定では空。README を参照)

### 🚀 インストール方法
1. ZIPファイルをダウンロード
2. 任意のフォルダに展開
3. `NetBelt.exe` をダブルクリックして起動

### 💻 動作環境
- Windows 10 / Windows 11 (64bit)
- Pythonのインストールは不要
- すべての依存ファイルが含まれています

### ✨ 主な機能
- SSH、Telnet、シリアル接続に対応
- デバイス管理とグループ化
- マクロ機能
- SNMP Trap / Syslog 受信(カスタムMIB対応)
- SFTP ファイル転送

### 📝 初回起動
- 初回起動時に `config.json` が自動生成されます
- MIB ファイルは同梱していません。`mibs/README.md` の手順で配置してください

詳しい使い方は [README.md](https://github.com/gomanizm/NetBelt/blob/main/README.md) をご覧ください。
body_path: release_notes.md
draft: false
prerelease: false
env:
Expand Down
78 changes: 78 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,84 @@
このファイルの形式は [Keep a Changelog](https://keepachangelog.com/ja/1.1.0/) に、
バージョン番号は [Semantic Versioning](https://semver.org/lang/ja/) に従います。

## [1.2.0] - 2026-08-27

ターミナルの描画を作り直しました。これまでは受信した文字を書き足していく
だけで、行と桁からなる「画面」という概念を持っていませんでした。そのため
`nano` や `vi` が「23 行目の 1 桁目へ」と指示しても行き先が無く、画面ぶんの
描画がすべて同じ場所に重なっていました。

**設定や接続先の作り直しは不要です。** これまでの `config.json` をそのまま
使えます。

### 追加
- **`nano` や `vi` の画面が崩れなくなった。** これまでは「対応していません」と
案内を出すのが精一杯でした。タイトルバー・編集領域・下部のショートカット
2 段が、実際の画面として組み上がります。`less` や `top` のような画面を
作るコマンドも同じ仕組みで扱いますが、すべてのコマンドを試したわけでは
ないため、崩れるものが残っている可能性はあります。
- **全画面のコマンドの画面は、記録にも残ります。** `nano` や `vi` を閉じても
開く前の画面は戻らず、編集中の画面がそのままスクロールバックと
セッションログに残ります。NetBelt は端末種別を `vt100` として接続するため、
これらのコマンドが画面を退避する仕組み(代替画面)を使わないためです。
編集内容を記録に残したくない場合は、ログ記録を止めてください
(スクロールバックには残りますが、画面上だけでファイルには残りません)。
- **色と装飾を表示するようになった。** 文字色・背景色・太字・下線・反転に
対応しました。基本 16 色に加えて 256 色パレットと RGB 直接指定も扱えます。
反転表示は、設定した文字色と背景色を入れ替えて表示します。
- **ウィンドウの大きさを機器へ伝えるようになった。** これまでは 80×24 固定で、
ウィンドウを広げても `show` の出力が 80 桁で折り返していました。接続時と
リサイズ時に実際の行数・桁数を伝えます(RFC 4254 §6.2 / §6.7)。
この通知は SSH のみです。Telnet とシリアルでは通知しませんが、
画面の描画・色・折り返しはこれまでどおり同じように働きます。
- **アプリケーションが指定する入力方式に従うようになった。** `nano` が
要求するカーソルキーの形式(DECCKM)と、bash のブラケットペーストに
対応しました。貼り付けが 1 行ずつ実行されず、まとめて編集できます。
- **ステータスバーに端末の大きさを表示するようにした。** いま表示中の
ターミナルの桁数と行数が右端に出ます(SSH ではこの値を機器へ伝えて
います。Telnet とシリアルでは表示だけです)。
- **ターミナルのタブをドラッグで並べ替えられるようにした。**
- **接続先リストを隠せるようにした。** ツールエリアと同じく、仕切りを
ドラッグする・右クリックする・表示メニューから切り替える、の 3 通りです。
- **更新の通知に、何が変わったかを出すようにした。** これまでは使い方の
案内しか出ておらず、更新して何が変わるのか分かりませんでした。
何世代か飛ばしている場合は、その間のぶんもまとめて表示します
(多すぎると読まれないため、5 世代までとし、残りは件数だけ添えます)。

### 修正
- **`nano` や `vi` を開くと画面が 1 行に潰れていた。** 上の「追加」の
とおり、画面として組み上がるようになりました。
- **ウィンドウを広げても、出力が 80 桁で折り返したままだった。**
端末の大きさを 80×24 に固定して機器へ伝えていたためです。

### 変更
- 受信の解釈を、正規表現による逐次処理から仕様どおりの状態機械へ置き換え
ました。受信がどこで途切れても解釈が変わりません。実機から採取した
データを分割して検証しています(Linux 側の 3 種は全ての切れ目、
機器から採った 7058 バイトは 7 バイトおき)。
- 画面の大きさが変わったとき、すでに書かれた行は組み直しません(実端末と
同じ挙動です)。狭い状態で出力された行は、その折り返しのまま残ります。
ただし表示領域から流れて記録に入った行は 1 本の行として保存されるため、
ウィンドウの幅に合わせて折り返し直されます。
- 全画面アプリを検知して案内を出す仕組みを廃止しました。実際に表示できる
ようになったため不要になりました。
- 機器から端末への問い合わせ(カーソル位置・装置種別)に、自動で答える
ようになりました。答えないと待ち続けるアプリケーションがあるためです。
送るのは問い合わせへの応答だけで、こちらから何かを送ることはありません。
- 端末の高さを機器へ伝えるようになったため、機器側のページャ(`--More--`)
の区切りがウィンドウの高さで変わります。ウィンドウが高いほど一度に
多く表示されます。
- 全角文字は 1 桁として数えます。日本語を含む行の折り返し位置が、
実際の見た目とずれることがあります。機器の出力は英数字が中心のため、
影響は Linux 側での日本語編集に限られます。

### 開発メモ
作り直しは仕様から行っています。既存のターミナル実装のソースコードは
読んでいません(読んで書き直すと、構造や命名が似たときに派生物とみなされる
余地が残るため)。参照したのは Paul Williams の DEC 互換パーサ状態遷移図、
ECMA-48(= ISO/IEC 6429)、ISO/IEC 2022、RFC 4254、および標準に無い部分に
ついては XTerm Control Sequences です。

## [1.1.1] - 2026-08-26

公開後に見つかった不具合の修正です。
Expand Down
2 changes: 1 addition & 1 deletion src/__version__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@
NetBelt バージョン情報
"""

__version__ = "1.1.1"
__version__ = "1.2.0"
GITHUB_REPO = "gomanizm/NetBelt"
APP_NAME = "NetBelt"
7 changes: 4 additions & 3 deletions src/core/serial_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ def connect(self) -> bool:
self._should_stop = False

# 接続成功メッセージ
self.output_received.emit(f"\n接続しました: {self.port} ({self.baudrate} baud)\n")
self.output_received.emit(
f"\r\n接続しました: {self.port} ({self.baudrate} baud)\r\n")
self.connected.emit()

# 読み取りスレッドを開始
Expand All @@ -73,12 +74,12 @@ def connect(self) -> bool:

except serial.SerialException as e:
error_msg = f"接続失敗: {str(e)}"
self.output_received.emit(f"\n{error_msg}\n")
self.output_received.emit(f"\r\n{error_msg}\r\n")
self.error_occurred.emit(error_msg)
return False
except Exception as e:
error_msg = f"予期しないエラー: {str(e)}"
self.output_received.emit(f"\n{error_msg}\n")
self.output_received.emit(f"\r\n{error_msg}\r\n")
self.error_occurred.emit(error_msg)
return False

Expand Down
25 changes: 22 additions & 3 deletions src/core/ssh_connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ def __init__(self, host: str, port: int, username: str, password: str = "",
self.client: Optional[paramiko.SSHClient] = None
self.channel: Optional[paramiko.Channel] = None
self.is_connected = False
# 端末の大きさ。接続前に set_terminal_size で上書きされる
self.term_cols = 80
self.term_rows = 24
self._read_thread: Optional[threading.Thread] = None
self._stop_reading = False

Expand Down Expand Up @@ -166,8 +169,9 @@ def connect(self) -> bool:
# SSH接続を実行
self.client.connect(**connect_kwargs)

# インタラクティブシェルを開始
self.channel = self.client.invoke_shell(term='vt100', width=80, height=24)
# インタラクティブシェルを開始 (RFC 4254 6.2 pty-req)
self.channel = self.client.invoke_shell(
term='vt100', width=self.term_cols, height=self.term_rows)
self.channel.settimeout(0.1)

self.is_connected = True
Expand Down Expand Up @@ -230,7 +234,22 @@ def send_command(self, command: str):
self.channel.send(command.encode('utf-8'))
except Exception as e:
self.error_occurred.emit(f"送信エラー: {str(e)}")


def set_terminal_size(self, cols: int, rows: int):
"""端末の大きさを機器へ伝える (RFC 4254 6.7 window-change)。

接続前に呼ばれたら、接続時の pty 要求 (6.2) に使う。
通知に失敗しても接続はそのまま続ける。
"""
self.term_cols = cols
self.term_rows = rows
if not self.is_connected or not self.channel:
return
try:
self.channel.resize_pty(width=cols, height=rows)
except Exception:
pass

def _read_output(self):
"""バックグラウンドで出力を読み取る"""
while not self._stop_reading and self.is_connected:
Expand Down
1 change: 1 addition & 0 deletions src/core/terminal/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# 端末エミュレーション。パーサと画面は UI を知らない。
Loading