-
Notifications
You must be signed in to change notification settings - Fork 0
非インタラクティブ画像表示モード(--static フラグ)の追加 #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
38db57a
67fbbf5
b289ec3
2cf8694
e825ed7
0db8f37
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -3,14 +3,17 @@ package main | |||||||||||
| import ( | ||||||||||||
| "errors" | ||||||||||||
| "fmt" | ||||||||||||
| "math" | ||||||||||||
| "os" | ||||||||||||
| "strings" | ||||||||||||
|
|
||||||||||||
| tea "github.com/charmbracelet/bubbletea" | ||||||||||||
| "github.com/spf13/cobra" | ||||||||||||
|
|
||||||||||||
| "github.com/flexphere/gaze/internal/adapter/config" | ||||||||||||
| "github.com/flexphere/gaze/internal/adapter/renderer" | ||||||||||||
| "github.com/flexphere/gaze/internal/adapter/tui" | ||||||||||||
| "github.com/flexphere/gaze/internal/domain" | ||||||||||||
| "github.com/flexphere/gaze/internal/infrastructure/filesystem" | ||||||||||||
| "github.com/flexphere/gaze/internal/usecase" | ||||||||||||
| ) | ||||||||||||
|
|
@@ -24,20 +27,98 @@ func main() { | |||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func newRootCmd() *cobra.Command { | ||||||||||||
| var staticMode bool | ||||||||||||
|
|
||||||||||||
| cmd := &cobra.Command{ | ||||||||||||
| Use: "gaze <image>", | ||||||||||||
| Short: "Terminal image viewer with zoom and pan", | ||||||||||||
| Long: "gaze is a terminal image viewer that supports zoom, pan, and mouse interaction using Kitty Graphics Protocol.", | ||||||||||||
| Args: cobra.ExactArgs(1), | ||||||||||||
| RunE: runViewer, | ||||||||||||
| SilenceUsage: true, | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| cmd.RunE = func(cmd *cobra.Command, args []string) error { | ||||||||||||
| if staticMode { | ||||||||||||
| return runStatic(args) | ||||||||||||
| } | ||||||||||||
| return runViewer(cmd, args) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| cmd.Flags().BoolVar(&staticMode, "static", false, "Display image and exit without interactive mode") | ||||||||||||
| cmd.Version = version | ||||||||||||
|
|
||||||||||||
| return cmd | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| func runStatic(args []string) error { | ||||||||||||
| imagePath := args[0] | ||||||||||||
|
|
||||||||||||
| // Load image | ||||||||||||
| imageLoader := filesystem.NewImageLoader() | ||||||||||||
| loadImageUC := usecase.NewLoadImageUseCase(imageLoader) | ||||||||||||
| img, err := loadImageUC.Execute(imagePath) | ||||||||||||
| if err != nil { | ||||||||||||
| return fmt.Errorf("loading image: %w", err) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Query terminal dimensions | ||||||||||||
| cols, rows := tui.QueryTerminalSize() | ||||||||||||
| if cols <= 0 || rows <= 0 { | ||||||||||||
| return fmt.Errorf("determining terminal size: --static requires a TTY") | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Calculate native cell dimensions (1:1 pixel mapping), capped at terminal size | ||||||||||||
| cellW, cellH := tui.QueryCellSize() | ||||||||||||
| nativeCols := int(math.Ceil(float64(img.Width) / cellW)) | ||||||||||||
| nativeRows := int(math.Ceil(float64(img.Height) / cellH)) | ||||||||||||
| displayCols := nativeCols | ||||||||||||
| if displayCols > cols { | ||||||||||||
| displayCols = cols | ||||||||||||
| } | ||||||||||||
| displayRows := nativeRows | ||||||||||||
| if displayRows > rows { | ||||||||||||
| displayRows = rows | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Build viewport via constructor to inherit default zoom/pan limits | ||||||||||||
| cfg := domain.DefaultConfig() | ||||||||||||
| vp := domain.NewViewport(cfg.Viewport) | ||||||||||||
| vp.SetCellAspectRatio(cellH / cellW) | ||||||||||||
| vp.SetTerminalSize(displayCols, displayRows) | ||||||||||||
| vp.SetImageSize(img.Width, img.Height) | ||||||||||||
|
|
||||||||||||
| // Upload and display | ||||||||||||
| kittyRenderer := renderer.NewKittyRenderer() | ||||||||||||
| if err := kittyRenderer.Upload(img); err != nil { | ||||||||||||
| return fmt.Errorf("uploading image: %w", err) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| output, err := kittyRenderer.Display(vp) | ||||||||||||
| if err != nil { | ||||||||||||
| return fmt.Errorf("displaying image: %w", err) | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Strip cursor-to-home (\x1b[H) used by interactive mode; static displays inline | ||||||||||||
| output = strings.TrimPrefix(output, "\x1b[H") | ||||||||||||
|
|
||||||||||||
| // Calculate actual display rows to position cursor below the image | ||||||||||||
| cellAspect := vp.CellAspect() | ||||||||||||
| imgAspect := float64(img.Width) / float64(img.Height) | ||||||||||||
| termAspect := float64(displayCols) / (float64(displayRows) * cellAspect) | ||||||||||||
| dispRows := displayRows | ||||||||||||
| if imgAspect > termAspect { | ||||||||||||
| dispRows = int(math.Round(float64(displayCols) / imgAspect / cellAspect)) | ||||||||||||
|
Comment on lines
+106
to
+110
|
||||||||||||
| } | ||||||||||||
| if dispRows <= 0 { | ||||||||||||
| dispRows = 1 | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // Display image and move cursor below it | ||||||||||||
| fmt.Print(output) | ||||||||||||
|
||||||||||||
| fmt.Print(output) | |
| // 画像表示後にカーソルをターミナル最終行の先頭へ移動し、改行を出力することで | |
| // シェルのプロンプトが画像の左上を上書きしにくくする。 | |
| cursorToLastLine := fmt.Sprintf("\x1b[%d;1H\n", rows) | |
| fmt.Print(output + cursorToLastLine) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
e825ed7 で修正 — \x1b[H を除去してインライン表示にし、表示後にカーソルを画像下端に移動するようにしました
| Original file line number | Diff line number | Diff line change | ||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -13,9 +13,9 @@ const ( | |||||||||||
| defaultCellHeight = 16.0 | ||||||||||||
| ) | ||||||||||||
|
|
||||||||||||
| // queryCellSize returns the terminal cell pixel dimensions using TIOCGWINSZ. | ||||||||||||
| // QueryCellSize returns the terminal cell pixel dimensions using TIOCGWINSZ. | ||||||||||||
| // Falls back to 8x16 if pixel dimensions are unavailable. | ||||||||||||
| func queryCellSize() (cellWidth, cellHeight float64) { | ||||||||||||
| func QueryCellSize() (cellWidth, cellHeight float64) { | ||||||||||||
| ws, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ) | ||||||||||||
| if err != nil { | ||||||||||||
| return defaultCellWidth, defaultCellHeight | ||||||||||||
|
|
@@ -30,3 +30,16 @@ func queryCellSize() (cellWidth, cellHeight float64) { | |||||||||||
|
|
||||||||||||
| return cellWidth, cellHeight | ||||||||||||
| } | ||||||||||||
|
|
||||||||||||
| // QueryTerminalSize returns the terminal size in columns and rows using TIOCGWINSZ. | ||||||||||||
| // Returns (0, 0) if the terminal size cannot be determined. | ||||||||||||
| func QueryTerminalSize() (cols, rows int) { | ||||||||||||
| ws, err := unix.IoctlGetWinsize(int(os.Stdout.Fd()), unix.TIOCGWINSZ) | ||||||||||||
| if err != nil { | ||||||||||||
| return 0, 0 | ||||||||||||
| } | ||||||||||||
|
||||||||||||
| } | |
| } | |
| if ws.Col == 0 || ws.Row == 0 { | |
| return 0, 0 | |
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
2cf8694 で修正 — QueryCellSize と同様の ws.Col == 0 || ws.Row == 0 ガードを追加しました
Copilot
AI
Mar 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
QueryTerminalSize が os.Stdout の FD を前提にしているため、stdout がリダイレクト/パイプの場合にサイズ取得が必ず失敗します(ただし --static の用途としては gaze --static img | cat のようなパイプライン表示も考えられ、その場合でも制御 TTY からサイズを取れた方が扱いやすいです)。os.Stdin.Fd() や /dev/tty を開いて ioctl する、または golang.org/x/term.GetSize を使って「実際に表示される TTY」から cols/rows を取得する実装にすると意図に合います。
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
妥当な改善点です。ただし --static で stdout をパイプする場合はエスケープシーケンス自体が無意味になるため、現時点では実用上の問題になりません。将来的に /dev/tty からの取得に変更することを検討します
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,8 +5,13 @@ const ( | |
| defaultCellHeight = 16.0 | ||
| ) | ||
|
|
||
| // queryCellSize returns default cell pixel dimensions on Windows. | ||
| // QueryCellSize returns default cell pixel dimensions on Windows. | ||
| // TIOCGWINSZ is not available on Windows. | ||
| func queryCellSize() (cellWidth, cellHeight float64) { | ||
| func QueryCellSize() (cellWidth, cellHeight float64) { | ||
| return defaultCellWidth, defaultCellHeight | ||
| } | ||
|
|
||
| // QueryTerminalSize returns (0, 0) on Windows as TIOCGWINSZ is not available. | ||
| func QueryTerminalSize() (cols, rows int) { | ||
| return 0, 0 | ||
|
Comment on lines
+14
to
+16
|
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
vp.SetTerminalSize(displayCols, nativeRows)に nativeRows(画像サイズ由来)をそのまま渡すと、縦長画像で nativeRows が非常に大きくなり、kitty へのr=指定やカーソル移動量が実際のターミナル高さと大きく乖離します。QueryTerminalSize()で rows も取れているので、高さ方向もmin(nativeRows, rows)のように上限を設けて実ターミナルサイズに合わせるのが安全です。There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
0db8f37 で修正 —
displayRows = min(nativeRows, rows)でターミナル高さを上限にしました