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
76 changes: 72 additions & 4 deletions CodeApp/Managers/TerminalInstance.swift
Original file line number Diff line number Diff line change
Expand Up @@ -233,14 +233,47 @@ class TerminalInstance: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
if let input = result["Input"] as? String {
ts.write(data: "\(input)".data(using: .utf8)!)
}
case "ControlReset":
let generation = result["Generation"] as? Int ?? 0
NotificationCenter.default.post(
name: .terminalControlReset,
object: self,
userInfo: ["generation": generation]
)
case "AltReset":
let generation = result["Generation"] as? Int ?? 0
NotificationCenter.default.post(
name: .terminalAltReset,
object: self,
userInfo: ["generation": generation]
)
default:
return
}
return
}

if self.executor?.state == .interactive && event == "Data" {
self.executor?.sendInput(input: result["Input"] as! String)
if self.executor?.state == .interactive {
switch event {
case "Data":
self.executor?.sendInput(input: result["Input"] as! String)
case "ControlReset":
let generation = result["Generation"] as? Int ?? 0
NotificationCenter.default.post(
name: .terminalControlReset,
object: self,
userInfo: ["generation": generation]
)
case "AltReset":
let generation = result["Generation"] as? Int ?? 0
NotificationCenter.default.post(
name: .terminalAltReset,
object: self,
userInfo: ["generation": generation]
)
default:
break
}
return
}

Expand Down Expand Up @@ -368,6 +401,20 @@ class TerminalInstance: NSObject, WKScriptMessageHandler, WKNavigationDelegate {
executor?.kill()
}
}
case "ControlReset":
let generation = result["Generation"] as? Int ?? 0
NotificationCenter.default.post(
name: .terminalControlReset,
object: self,
userInfo: ["generation": generation]
)
case "AltReset":
let generation = result["Generation"] as? Int ?? 0
NotificationCenter.default.post(
name: .terminalAltReset,
object: self,
userInfo: ["generation": generation]
)
default:
print("\(result) Event not handled")
}
Expand Down Expand Up @@ -493,10 +540,31 @@ extension TerminalInstance: WKUIDelegate {
extension TerminalInstance {
func type(text: String) {
guard let base64 = text.base64Encoded() else { return }
executeScript("term.input(base64ToString(`\(base64)`))")
executeScript("inputWithModifiers(base64ToString(`\(base64)`))")
}

func moveCursor(codeSequence: String) {
executeScript("term.input(String.fromCharCode(0x1b)+'\(codeSequence)')")
executeScript("inputWithModifiers(String.fromCharCode(0x1b)+'\(codeSequence)')")
}

func setControlActive(_ active: Bool, generation: Int) {
executeScript("setControlActive(\(active), \(generation))")
}

func setControlLocked(_ locked: Bool) {
executeScript("setControlLocked(\(locked))")
}

func setAltActive(_ active: Bool, generation: Int) {
executeScript("setAltActive(\(active), \(generation))")
}

func setAltLocked(_ locked: Bool) {
executeScript("setAltLocked(\(locked))")
}
}

extension Notification.Name {
static let terminalControlReset = Notification.Name("terminalControlReset")
static let terminalAltReset = Notification.Name("terminalAltReset")
}
191 changes: 184 additions & 7 deletions CodeApp/Views/TerminalKeyboardToolbar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,99 @@ struct TerminalKeyboardToolBar: View {
@EnvironmentObject var App: MainApp
@Environment(\.horizontalSizeClass) var horizontalSizeClass
@State var pasteBoardHasContent = false
@State var controlActive = false
@State var controlLocked = false
@State var controlLastTapTime: Date?
@State var controlGeneration = 0
@State var altActive = false
@State var altLocked = false
@State var altLastTapTime: Date?
@State var altGeneration = 0

private let doubleTapInterval: TimeInterval = 0.3

private func resetModifierStates() {
controlActive = false
controlLocked = false
App.terminalInstance.setControlActive(false, generation: controlGeneration)
App.terminalInstance.setControlLocked(false)
altActive = false
altLocked = false
App.terminalInstance.setAltActive(false, generation: altGeneration)
App.terminalInstance.setAltLocked(false)
}

private func resetUnlockedModifiers() {
// Reset modifiers only if they are not locked
if !controlLocked {
controlActive = false
App.terminalInstance.setControlActive(false, generation: controlGeneration)
}
if !altLocked {
altActive = false
App.terminalInstance.setAltActive(false, generation: altGeneration)
}
}

private func handleControlTap() {
let now = Date()
let isDoubleTap =
controlLastTapTime.map { now.timeIntervalSince($0) < doubleTapInterval } ?? false
controlLastTapTime = now

if controlLocked {
// Single tap while locked: unlock and deactivate
controlLocked = false
controlActive = false
controlGeneration += 1
App.terminalInstance.setControlLocked(false)
App.terminalInstance.setControlActive(false, generation: controlGeneration)
} else if isDoubleTap && controlActive {
// Double tap while active: lock
controlLocked = true
App.terminalInstance.setControlLocked(true)
} else {
// Single tap: toggle active state
controlActive.toggle()
controlGeneration += 1
App.terminalInstance.setControlActive(controlActive, generation: controlGeneration)
}
}

private func handleAltTap() {
let now = Date()
let isDoubleTap =
altLastTapTime.map { now.timeIntervalSince($0) < doubleTapInterval } ?? false
altLastTapTime = now

if altLocked {
// Single tap while locked: unlock and deactivate
altLocked = false
altActive = false
altGeneration += 1
App.terminalInstance.setAltLocked(false)
App.terminalInstance.setAltActive(false, generation: altGeneration)
} else if isDoubleTap && altActive {
// Double tap while active: lock
altLocked = true
App.terminalInstance.setAltLocked(true)
} else {
// Single tap: toggle active state
altActive.toggle()
altGeneration += 1
App.terminalInstance.setAltActive(altActive, generation: altGeneration)
}
}

private func typeAndResetModifiers(text: String) {
App.terminalInstance.type(text: text)
resetUnlockedModifiers()
}

private func moveCursorAndResetModifiers(codeSequence: String) {
App.terminalInstance.moveCursor(codeSequence: codeSequence)
resetUnlockedModifiers()
}

var body: some View {
HStack(spacing: horizontalSizeClass == .compact ? 8 : 14) {
Expand All @@ -20,7 +113,7 @@ struct TerminalKeyboardToolBar: View {
Button(
action: {
if let string = UIPasteboard.general.string {
App.terminalInstance.type(text: string)
typeAndResetModifiers(text: string)
}
},
label: {
Expand All @@ -29,46 +122,104 @@ struct TerminalKeyboardToolBar: View {
}
Button(
action: {
App.terminalInstance.type(text: "\t")
typeAndResetModifiers(text: "\u{1b}")
},
label: {
Text("Esc")
}
)
.accessibilityLabel("Escape")
Button(
action: {
typeAndResetModifiers(text: "\t")
},
label: {
Text("↹")
})
Button(
action: {
handleControlTap()
},
label: {
Text("Ctrl")
.padding(.horizontal, 4)
.background(
controlActive ? Color.accentColor.opacity(0.3) : Color.clear
)
.cornerRadius(4)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(Color.accentColor, lineWidth: controlLocked ? 2 : 0)
)
}
)
.accessibilityLabel("Control")
.accessibilityValue(
controlLocked ? "Locked" : (controlActive ? "Active" : "Inactive"))
Button(
action: {
handleAltTap()
},
label: {
Text("Alt")
.padding(.horizontal, 4)
.background(
altActive ? Color.accentColor.opacity(0.3) : Color.clear
)
.cornerRadius(4)
.overlay(
RoundedRectangle(cornerRadius: 4)
.stroke(Color.accentColor, lineWidth: altLocked ? 2 : 0)
)
}
)
.accessibilityLabel("Alt")
.accessibilityValue(altLocked ? "Locked" : (altActive ? "Active" : "Inactive"))
Button(
action: {
typeAndResetModifiers(text: "\u{1b}[3~")
},
label: {
Text("Del")
}
)
.accessibilityLabel("Delete")
}

Spacer()

Group {
Button(
action: {
App.terminalInstance.moveCursor(codeSequence: "[A")
moveCursorAndResetModifiers(codeSequence: "[A")
},
label: {
Image(systemName: "arrow.up")
})
Button(
action: {
App.terminalInstance.moveCursor(codeSequence: "[B")
moveCursorAndResetModifiers(codeSequence: "[B")
},
label: {
Image(systemName: "arrow.down")
})
Button(
action: {
App.terminalInstance.moveCursor(codeSequence: "[D")
moveCursorAndResetModifiers(codeSequence: "[D")
},
label: {
Image(systemName: "arrow.left")
})
Button(
action: {
App.terminalInstance.moveCursor(codeSequence: "[C")
moveCursorAndResetModifiers(codeSequence: "[C")
},
label: {
Image(systemName: "arrow.right")
})
Button(
action: {
resetModifierStates()
App.terminalInstance.blur()
},
label: {
Expand All @@ -84,12 +235,38 @@ struct TerminalKeyboardToolBar: View {
.ignoresSafeArea()
.onReceive(
NotificationCenter.default.publisher(for: UIPasteboard.changedNotification),
perform: { val in
perform: { _ in
if UIPasteboard.general.hasStrings {
pasteBoardHasContent = true
} else {
pasteBoardHasContent = false
}
}
)
.onReceive(
NotificationCenter.default.publisher(
for: .terminalControlReset,
object: App.terminalInstance
),
perform: { notification in
if let generation = notification.userInfo?["generation"] as? Int,
generation == controlGeneration
{
controlActive = false
}
}
)
.onReceive(
NotificationCenter.default.publisher(
for: .terminalAltReset,
object: App.terminalInstance
),
perform: { notification in
if let generation = notification.userInfo?["generation"] as? Int,
generation == altGeneration
{
altActive = false
}
})
}
}
Loading