mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-09-28 22:18:36 +00:00
macOS: Progress bar for OSC9 progress reports
This commit is contained in:
@@ -722,15 +722,15 @@ typedef enum {
|
|||||||
GHOSTTY_PROGRESS_STATE_ERROR,
|
GHOSTTY_PROGRESS_STATE_ERROR,
|
||||||
GHOSTTY_PROGRESS_STATE_INDETERMINATE,
|
GHOSTTY_PROGRESS_STATE_INDETERMINATE,
|
||||||
GHOSTTY_PROGRESS_STATE_PAUSE,
|
GHOSTTY_PROGRESS_STATE_PAUSE,
|
||||||
} ghostty_terminal_osc_command_progressreport_state_e;
|
} ghostty_action_progress_report_state_e;
|
||||||
|
|
||||||
// terminal.osc.Command.ProgressReport.C
|
// terminal.osc.Command.ProgressReport.C
|
||||||
typedef struct {
|
typedef struct {
|
||||||
ghostty_terminal_osc_command_progressreport_state_e state;
|
ghostty_action_progress_report_state_e state;
|
||||||
// -1 if no progress was reported, otherwise 0-100 indicating percent
|
// -1 if no progress was reported, otherwise 0-100 indicating percent
|
||||||
// completeness.
|
// completeness.
|
||||||
int8_t progress;
|
int8_t progress;
|
||||||
} ghostty_terminal_osc_command_progressreport_s;
|
} ghostty_action_progress_report_s;
|
||||||
|
|
||||||
// apprt.Action.Key
|
// apprt.Action.Key
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@@ -817,7 +817,7 @@ typedef union {
|
|||||||
ghostty_action_open_url_s open_url;
|
ghostty_action_open_url_s open_url;
|
||||||
ghostty_action_close_tab_mode_e close_tab_mode;
|
ghostty_action_close_tab_mode_e close_tab_mode;
|
||||||
ghostty_surface_message_childexited_s child_exited;
|
ghostty_surface_message_childexited_s child_exited;
|
||||||
ghostty_terminal_osc_command_progressreport_s progress_report;
|
ghostty_action_progress_report_s progress_report;
|
||||||
} ghostty_action_u;
|
} ghostty_action_u;
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
|
@@ -127,6 +127,7 @@
|
|||||||
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */; };
|
A5CF66D72D29DDB500139794 /* Ghostty.Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5CF66D62D29DDB100139794 /* Ghostty.Event.swift */; };
|
||||||
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */; };
|
A5D0AF3B2B36A1DE00D21823 /* TerminalRestorable.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3A2B36A1DE00D21823 /* TerminalRestorable.swift */; };
|
||||||
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3C2B37804400D21823 /* CodableBridge.swift */; };
|
A5D0AF3D2B37804400D21823 /* CodableBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5D0AF3C2B37804400D21823 /* CodableBridge.swift */; };
|
||||||
|
A5D689BE2E654D98002E2346 /* Ghostty.Action.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53A6C022CCC1B7D00943E98 /* Ghostty.Action.swift */; };
|
||||||
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; };
|
A5E112932AF73E6E00C6E0C2 /* ClipboardConfirmation.xib in Resources */ = {isa = PBXBuildFile; fileRef = A5E112922AF73E6E00C6E0C2 /* ClipboardConfirmation.xib */; };
|
||||||
A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; };
|
A5E112952AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112942AF73E8A00C6E0C2 /* ClipboardConfirmationController.swift */; };
|
||||||
A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; };
|
A5E112972AF7401B00C6E0C2 /* ClipboardConfirmationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5E112962AF7401B00C6E0C2 /* ClipboardConfirmationView.swift */; };
|
||||||
@@ -987,6 +988,7 @@
|
|||||||
A5333E232B5A219A008AEFF7 /* SurfaceView.swift in Sources */,
|
A5333E232B5A219A008AEFF7 /* SurfaceView.swift in Sources */,
|
||||||
A5333E202B5A2111008AEFF7 /* SurfaceView_UIKit.swift in Sources */,
|
A5333E202B5A2111008AEFF7 /* SurfaceView_UIKit.swift in Sources */,
|
||||||
A5333E1D2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
|
A5333E1D2B5A1CE3008AEFF7 /* CrossKit.swift in Sources */,
|
||||||
|
A5D689BE2E654D98002E2346 /* Ghostty.Action.swift in Sources */,
|
||||||
A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */,
|
A53D0C9C2B543F7B00305CE6 /* Package.swift in Sources */,
|
||||||
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */,
|
A53D0C9B2B543F3B00305CE6 /* Ghostty.App.swift in Sources */,
|
||||||
A5333E242B5A22D9008AEFF7 /* Ghostty.Shell.swift in Sources */,
|
A5333E242B5A22D9008AEFF7 /* Ghostty.Shell.swift in Sources */,
|
||||||
|
@@ -70,4 +70,39 @@ extension Ghostty.Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ProgressReport {
|
||||||
|
enum State {
|
||||||
|
case remove
|
||||||
|
case set
|
||||||
|
case error
|
||||||
|
case indeterminate
|
||||||
|
case pause
|
||||||
|
|
||||||
|
init(_ c: ghostty_action_progress_report_state_e) {
|
||||||
|
switch c {
|
||||||
|
case GHOSTTY_PROGRESS_STATE_REMOVE:
|
||||||
|
self = .remove
|
||||||
|
case GHOSTTY_PROGRESS_STATE_SET:
|
||||||
|
self = .set
|
||||||
|
case GHOSTTY_PROGRESS_STATE_ERROR:
|
||||||
|
self = .error
|
||||||
|
case GHOSTTY_PROGRESS_STATE_INDETERMINATE:
|
||||||
|
self = .indeterminate
|
||||||
|
case GHOSTTY_PROGRESS_STATE_PAUSE:
|
||||||
|
self = .pause
|
||||||
|
default:
|
||||||
|
self = .remove
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let state: State
|
||||||
|
let progress: UInt8?
|
||||||
|
|
||||||
|
init(c: ghostty_action_progress_report_s) {
|
||||||
|
self.state = State(c.state)
|
||||||
|
self.progress = c.progress >= 0 ? UInt8(c.progress) : nil
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -543,6 +543,9 @@ extension Ghostty {
|
|||||||
|
|
||||||
case GHOSTTY_ACTION_KEY_SEQUENCE:
|
case GHOSTTY_ACTION_KEY_SEQUENCE:
|
||||||
keySequence(app, target: target, v: action.action.key_sequence)
|
keySequence(app, target: target, v: action.action.key_sequence)
|
||||||
|
|
||||||
|
case GHOSTTY_ACTION_PROGRESS_REPORT:
|
||||||
|
progressReport(app, target: target, v: action.action.progress_report)
|
||||||
|
|
||||||
case GHOSTTY_ACTION_CONFIG_CHANGE:
|
case GHOSTTY_ACTION_CONFIG_CHANGE:
|
||||||
configChange(app, target: target, v: action.action.config_change)
|
configChange(app, target: target, v: action.action.config_change)
|
||||||
@@ -1523,6 +1526,33 @@ extension Ghostty {
|
|||||||
assertionFailure()
|
assertionFailure()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func progressReport(
|
||||||
|
_ app: ghostty_app_t,
|
||||||
|
target: ghostty_target_s,
|
||||||
|
v: ghostty_action_progress_report_s) {
|
||||||
|
switch (target.tag) {
|
||||||
|
case GHOSTTY_TARGET_APP:
|
||||||
|
Ghostty.logger.warning("progress report does nothing with an app target")
|
||||||
|
return
|
||||||
|
|
||||||
|
case GHOSTTY_TARGET_SURFACE:
|
||||||
|
guard let surface = target.target.surface else { return }
|
||||||
|
guard let surfaceView = self.surfaceView(from: surface) else { return }
|
||||||
|
|
||||||
|
let progressReport = Ghostty.Action.ProgressReport(c: v)
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if progressReport.state == .remove {
|
||||||
|
surfaceView.progressReport = nil
|
||||||
|
} else {
|
||||||
|
surfaceView.progressReport = progressReport
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
assertionFailure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static func configReload(
|
private static func configReload(
|
||||||
_ app: ghostty_app_t,
|
_ app: ghostty_app_t,
|
||||||
|
@@ -113,6 +113,11 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.ghosttySurfaceView(surfaceView)
|
.ghosttySurfaceView(surfaceView)
|
||||||
|
|
||||||
|
// Progress report overlay
|
||||||
|
if let progressReport = surfaceView.progressReport {
|
||||||
|
ProgressReportOverlay(report: progressReport)
|
||||||
|
}
|
||||||
|
|
||||||
#if canImport(AppKit)
|
#if canImport(AppKit)
|
||||||
// If we are in the middle of a key sequence, then we show a visual element. We only
|
// If we are in the middle of a key sequence, then we show a visual element. We only
|
||||||
@@ -267,6 +272,49 @@ extension Ghostty {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Progress report overlay that shows a progress bar at the top of the terminal
|
||||||
|
struct ProgressReportOverlay: View {
|
||||||
|
let report: Action.ProgressReport
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var progressBar: some View {
|
||||||
|
if let progress = report.progress {
|
||||||
|
// Determinate progress bar
|
||||||
|
ProgressView(value: Double(progress), total: 100)
|
||||||
|
.progressViewStyle(.linear)
|
||||||
|
.tint(report.state == .error ? .red : report.state == .pause ? .orange : nil)
|
||||||
|
.animation(.easeInOut(duration: 0.2), value: progress)
|
||||||
|
} else {
|
||||||
|
// Indeterminate states
|
||||||
|
switch report.state {
|
||||||
|
case .indeterminate:
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.linear)
|
||||||
|
case .error:
|
||||||
|
ProgressView()
|
||||||
|
.progressViewStyle(.linear)
|
||||||
|
.tint(.red)
|
||||||
|
case .pause:
|
||||||
|
Rectangle().fill(Color.orange)
|
||||||
|
default:
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
progressBar
|
||||||
|
.scaleEffect(x: 1, y: 0.5, anchor: .center)
|
||||||
|
.frame(height: 2)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
||||||
|
.allowsHitTesting(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// This is the resize overlay that shows on top of a surface to show the current
|
// This is the resize overlay that shows on top of a surface to show the current
|
||||||
// size during a resize operation.
|
// size during a resize operation.
|
||||||
struct SurfaceResizeOverlay: View {
|
struct SurfaceResizeOverlay: View {
|
||||||
|
@@ -41,6 +41,23 @@ extension Ghostty {
|
|||||||
|
|
||||||
// The hovered URL string
|
// The hovered URL string
|
||||||
@Published var hoverUrl: String? = nil
|
@Published var hoverUrl: String? = nil
|
||||||
|
|
||||||
|
// The progress report (if any)
|
||||||
|
@Published var progressReport: Action.ProgressReport? = nil {
|
||||||
|
didSet {
|
||||||
|
// Cancel any existing timer
|
||||||
|
progressReportTimer?.invalidate()
|
||||||
|
progressReportTimer = nil
|
||||||
|
|
||||||
|
// If we have a new progress report, start a timer to remove it after 15 seconds
|
||||||
|
if progressReport != nil {
|
||||||
|
progressReportTimer = Timer.scheduledTimer(withTimeInterval: 15.0, repeats: false) { [weak self] _ in
|
||||||
|
self?.progressReport = nil
|
||||||
|
self?.progressReportTimer = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// The currently active key sequence. The sequence is not active if this is empty.
|
// The currently active key sequence. The sequence is not active if this is empty.
|
||||||
@Published var keySequence: [KeyboardShortcut] = []
|
@Published var keySequence: [KeyboardShortcut] = []
|
||||||
@@ -142,6 +159,9 @@ extension Ghostty {
|
|||||||
|
|
||||||
// A timer to fallback to ghost emoji if no title is set within the grace period
|
// A timer to fallback to ghost emoji if no title is set within the grace period
|
||||||
private var titleFallbackTimer: Timer?
|
private var titleFallbackTimer: Timer?
|
||||||
|
|
||||||
|
// Timer to remove progress report after 15 seconds
|
||||||
|
private var progressReportTimer: Timer?
|
||||||
|
|
||||||
// This is the title from the terminal. This is nil if we're currently using
|
// This is the title from the terminal. This is nil if we're currently using
|
||||||
// the terminal title as the main title property. If the title is set manually
|
// the terminal title as the main title property. If the title is set manually
|
||||||
@@ -348,6 +368,9 @@ extension Ghostty {
|
|||||||
// Remove any notifications associated with this surface
|
// Remove any notifications associated with this surface
|
||||||
let identifiers = Array(self.notificationIdentifiers)
|
let identifiers = Array(self.notificationIdentifiers)
|
||||||
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)
|
UNUserNotificationCenter.current().removeDeliveredNotifications(withIdentifiers: identifiers)
|
||||||
|
|
||||||
|
// Cancel progress report timer
|
||||||
|
progressReportTimer?.invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func focusDidChange(_ focused: Bool) {
|
func focusDidChange(_ focused: Bool) {
|
||||||
|
@@ -30,6 +30,9 @@ extension Ghostty {
|
|||||||
|
|
||||||
// The hovered URL
|
// The hovered URL
|
||||||
@Published var hoverUrl: String? = nil
|
@Published var hoverUrl: String? = nil
|
||||||
|
|
||||||
|
// The progress report (if any)
|
||||||
|
@Published var progressReport: Action.ProgressReport? = nil
|
||||||
|
|
||||||
// The time this surface last became focused. This is a ContinuousClock.Instant
|
// The time this surface last became focused. This is a ContinuousClock.Instant
|
||||||
// on supported platforms.
|
// on supported platforms.
|
||||||
|
@@ -211,7 +211,6 @@ pub const Command = union(enum) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pub const ProgressReport = struct {
|
pub const ProgressReport = struct {
|
||||||
// sync with ghostty_terminal_osc_command_progressreport_state_e in include/ghostty.h
|
|
||||||
pub const State = enum(c_int) {
|
pub const State = enum(c_int) {
|
||||||
remove,
|
remove,
|
||||||
set,
|
set,
|
||||||
@@ -223,7 +222,7 @@ pub const Command = union(enum) {
|
|||||||
state: State,
|
state: State,
|
||||||
progress: ?u8 = null,
|
progress: ?u8 = null,
|
||||||
|
|
||||||
// sync with ghostty_terminal_osc_command_progressreport_s in include/ghostty.h
|
// sync with ghostty_action_progress_report_s
|
||||||
pub const C = extern struct {
|
pub const C = extern struct {
|
||||||
state: c_int,
|
state: c_int,
|
||||||
progress: i8,
|
progress: i8,
|
||||||
@@ -232,7 +231,11 @@ pub const Command = union(enum) {
|
|||||||
pub fn cval(self: ProgressReport) C {
|
pub fn cval(self: ProgressReport) C {
|
||||||
return .{
|
return .{
|
||||||
.state = @intFromEnum(self.state),
|
.state = @intFromEnum(self.state),
|
||||||
.progress = if (self.progress) |progress| @intCast(std.math.clamp(progress, 0, 100)) else -1,
|
.progress = if (self.progress) |progress| @intCast(std.math.clamp(
|
||||||
|
progress,
|
||||||
|
0,
|
||||||
|
100,
|
||||||
|
)) else -1,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Reference in New Issue
Block a user