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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,21 @@ carry breaking changes.

## [Unreleased]

## [0.1.1] - 2026-08-06

### Fixed
- iOS: **swiping Files → Viewer (or container 6 → Terminal) no longer freezes the shell.**
Landing beside the Graph container mounts it as an off-screen edge preview, and mounting it
ran `git status` — which read and SHA-1 hashed every byte of every file in the workspace,
synchronously, on the main thread. The worktree diff now runs off the main thread behind a
(size, mtime) hash cache, and the walk holds one file's bytes at a time instead of the whole
tree's. New `verify/gitstatus` gate covers the ways the cache could lie.
- iOS: **the key strip no longer dismisses the keyboard mid-program.** Tapping up/down/left/
right/esc/tab on a running TUI resigned the keyboard on every press — the strip's buttons
read as "tap outside a text input" to the global tap-to-dismiss recognizer. While a program
runs, the terminal registers a keyboard-hold region the recognizer skips; tapping outside
the container still dismisses.

## [0.1.0] - 2026-08-05

### Added
Expand Down
4 changes: 2 additions & 2 deletions kotlin/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ android {
applicationId = "com.reagentsystems.mouse"
minSdk = 26
targetSdk = 35
versionCode = 1
versionName = "0.1"
versionCode = 2
versionName = "0.1.1"
}

buildTypes {
Expand Down
4 changes: 2 additions & 2 deletions swift/Mouse.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.1;
MARKETING_VERSION = 0.1.1;
OTHER_LDFLAGS = "-lz -lresolv";
PRODUCT_BUNDLE_IDENTIFIER = com.reagentsystems.mouse.swift;
SDKROOT = iphoneos;
Expand All @@ -307,7 +307,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 0.1;
MARKETING_VERSION = 0.1.1;
OTHER_LDFLAGS = "-lz -lresolv";
PRODUCT_BUNDLE_IDENTIFIER = com.reagentsystems.mouse.swift;
SDKROOT = iphoneos;
Expand Down
24 changes: 23 additions & 1 deletion swift/Mouse/MouseApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,37 @@ private struct KeyboardFloatingHost<Content: View>: UIViewControllerRepresentabl
}
}

/// Window regions where a tap must NOT dismiss the keyboard.
///
/// The dismiss recognizer below fires on any tap outside a text input, and for most of the app
/// that is the right reading of a tap. A terminal running a program is the exception: its key
/// strip and screen are buttons and output, not text inputs, yet tapping them is how the program
/// is DRIVEN — an arrow tap that also drops the keyboard makes a TUI menu undrivable one
/// keystroke at a time, which is exactly how it read on a phone. So the terminal registers its
/// frame while a program runs, and taps inside any registered region keep the keyboard. Taps
/// outside — another container, the gap between lanes — still dismiss, which is the half of the
/// rule worth keeping.
@MainActor
enum KeyboardHoldRegions {
private static var frames: [UUID: CGRect] = [:]
static func set(_ id: UUID, frame: CGRect) { frames[id] = frame }
static func clear(_ id: UUID) { frames[id] = nil }
static func contains(_ point: CGPoint) -> Bool {
frames.values.contains { $0.contains(point) }
}
}

final class KeyboardDismissCoordinator: NSObject, UIGestureRecognizerDelegate {
@objc func dismissKeyboard() {
UIApplication.shared.sendAction(
#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil
)
}

/// Skip taps that land inside a text input (or one of its subviews).
/// Skip taps that land inside a text input (or one of its subviews), and taps inside a
/// keyboard-hold region (a terminal with a program running — see [KeyboardHoldRegions]).
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
if KeyboardHoldRegions.contains(touch.location(in: nil)) { return false }
var view = touch.view
while let current = view {
if current is UITextInput { return false }
Expand Down
18 changes: 18 additions & 0 deletions swift/Mouse/Terminal.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ struct TerminalContainerView: View {
let deck: CarouselDeck?

@State private var promptFocused = false
/// This container's entry in [KeyboardHoldRegions] while a program runs.
@State private var holdRegionID = UUID()

var body: some View {
Group {
Expand Down Expand Up @@ -93,6 +95,22 @@ struct TerminalContainerView: View {
.padding(16)
.contentShape(Rectangle())
.onTapGesture { promptFocused = true }
// While a program runs, taps in this container are PROGRAM INPUT (the key
// strip, the screen) and must not double as keyboard dismissal. The region
// follows the container's real frame, so an edge-preview terminal registers
// an off-screen rect no tap can land in.
.background {
if terminal.program != nil {
GeometryReader { geo in
Color.clear
.onAppear { KeyboardHoldRegions.set(holdRegionID, frame: geo.frame(in: .global)) }
.onChange(of: geo.frame(in: .global)) { _, frame in
KeyboardHoldRegions.set(holdRegionID, frame: frame)
}
.onDisappear { KeyboardHoldRegions.clear(holdRegionID) }
}
}
}
} else {
Text("open a project in the Files container")
.font(.custom(AppFont.asciiName, size: 14))
Expand Down
2 changes: 1 addition & 1 deletion swift/project.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ targets:
settings:
PRODUCT_BUNDLE_IDENTIFIER: com.reagentsystems.mouse.swift
# Keep in step with kotlin/app/build.gradle.kts versionName (see RELEASING.md).
MARKETING_VERSION: "0.1"
MARKETING_VERSION: "0.1.1"
CURRENT_PROJECT_VERSION: "1"
# libz (system zlib) for the git engine's packfile inflate — GitCore.inflateStream needs
# exact zlib-member delimiting, which the Compression framework can't do.
Expand Down
Loading