Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
30 changes: 30 additions & 0 deletions Sources/MikuCodeApp/Terminal/Core/TerminalPaging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,36 @@ enum TerminalPager {
}
}

/// Converts continuous scroll-wheel deltas into discrete page steps for the
/// page-based terminal display. Positive deltas (scrolling up) accumulate into
/// positive steps (earlier history); leftover delta below one step is carried
/// so slow scrolling still eventually turns the page.
struct TerminalScrollAccumulator {
private(set) var accumulated: CGFloat = 0
let stepThreshold: CGFloat

init(stepThreshold: CGFloat = 48) {
self.stepThreshold = max(1, stepThreshold)
}

/// Adds one event's delta and returns how many whole page steps it
/// produced (positive = toward older history, negative = toward latest).
mutating func steps(adding delta: CGFloat) -> Int {
// Direction reversals discard the opposite-direction remainder so a
// flick back down is never dampened by leftover upward delta.
if delta > 0, accumulated < 0 { accumulated = 0 }
if delta < 0, accumulated > 0 { accumulated = 0 }
accumulated += delta
let steps = Int(accumulated / stepThreshold)
accumulated -= CGFloat(steps) * stepThreshold
return steps
}

mutating func reset() {
accumulated = 0
}
}

struct TerminalSearchMatch: Equatable, Sendable {
let lineIndex: Int
let column: Int
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,30 @@ final class TerminalOutputTextView: NSTextView {
super.mouseDown(with: event)
}

private var scrollAccumulator = TerminalScrollAccumulator()

/// The terminal renders fixed pages, so wheel/trackpad scrolling steps
/// through history page by page instead of moving the text view.
override func scrollWheel(with event: NSEvent) {
if event.phase == .began {
scrollAccumulator.reset()
}
// Legacy line-based wheels report small line deltas; scale them into
// point space so one physical notch is a meaningful fraction of a step.
let delta = event.hasPreciseScrollingDeltas
? event.scrollingDeltaY
: event.scrollingDeltaY * 16
let steps = scrollAccumulator.steps(adding: delta)
guard steps != 0 else { return }
for _ in 0 ..< abs(steps) {
if steps > 0 {
onPageUp?()
} else {
onPageDown?()
}
}
}

override func keyDown(with event: NSEvent) {
if event.keyCode == 116 {
onPageUp?()
Expand Down
4 changes: 4 additions & 0 deletions Sources/MikuCodeApp/Terminal/Rendering/TerminalPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,10 @@ struct TerminalPanel: View {
followsLatestPage = true
activeSearchMatchIndex = nil
activeSearchMatchIdentity = nil
// Clearing wipes the whole buffer, including the shell's prompt line —
// nudge the shell to repaint (zsh/bash clear-screen on Ctrl-L) so the
// terminal never sits fully blank after ⌘K.
session.sendRawInput(Data([0x0C]))
}

private func movePage(by delta: Int) {
Expand Down
29 changes: 29 additions & 0 deletions Tests/MikuCodeAppTests/TerminalPagingTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,35 @@ import XCTest
final class TerminalPagingTests: XCTestCase {
private let lines = (0 ..< 7).map { TerminalLine(text: "line \($0)") }

func testScrollAccumulatorTurnsDeltasIntoPageSteps() {
var accumulator = TerminalScrollAccumulator(stepThreshold: 48)

// Small deltas carry over until they add up to one step.
XCTAssertEqual(accumulator.steps(adding: 20), 0)
XCTAssertEqual(accumulator.steps(adding: 20), 0)
XCTAssertEqual(accumulator.steps(adding: 20), 1)

// A large flick produces multiple steps at once, keeping the remainder.
XCTAssertEqual(accumulator.steps(adding: 110), 2)
XCTAssertEqual(accumulator.steps(adding: 22), 1)

// Scrolling down yields negative steps.
XCTAssertEqual(accumulator.steps(adding: -96), -2)
}

func testScrollAccumulatorDropsOppositeRemainderOnDirectionChange() {
var accumulator = TerminalScrollAccumulator(stepThreshold: 48)

XCTAssertEqual(accumulator.steps(adding: 40), 0)
// Reversing direction must not be dampened by the leftover +40.
XCTAssertEqual(accumulator.steps(adding: -48), -1)

accumulator.reset()
XCTAssertEqual(accumulator.steps(adding: 47), 0)
accumulator.reset()
XCTAssertEqual(accumulator.steps(adding: 1), 0)
}

func testPageSlicesClampAndReportIndices() {
let first = TerminalPager.page(lines: lines, requestedPage: -2, pageSize: 3)
XCTAssertEqual(first.pageIndex, 0)
Expand Down