Skip to content

Commit

Permalink
implement uninstall (#89)
Browse files Browse the repository at this point in the history
  • Loading branch information
eagleoflqj authored Mar 23, 2024
1 parent 1c215c8 commit 3a01146
Show file tree
Hide file tree
Showing 4 changed files with 150 additions and 1 deletion.
5 changes: 5 additions & 0 deletions assets/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/fcitx.icns"
DESTINATION "${CMAKE_INSTALL_PREFIX}/Resources"
)

# Preserve execution permission
install(PROGRAMS "${CMAKE_CURRENT_SOURCE_DIR}/uninstall.sh"
DESTINATION "${CMAKE_INSTALL_PREFIX}/Resources"
)

install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/Base.lproj"
DESTINATION "${CMAKE_INSTALL_PREFIX}/Resources"
)
Expand Down
19 changes: 19 additions & 0 deletions assets/uninstall.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/zsh
set -xeu

user="$1"
remove_user_data="$2"
APP_DIR="/Library/Input Methods/Fcitx5.app"
DATA_DIR="/Users/$user/Library/fcitx5"
CONFIG_DIR="/Users/$user/.config/fcitx5"
LOCAL_DIR="/Users/$user/.local/share/fcitx5"

rm -rf "$APP_DIR"
rm -rf "$DATA_DIR"
rm -rf "$CONFIG_DIR"

if [ "$remove_user_data" = "true" ]; then
rm -rf "$LOCAL_DIR"
fi

killall Fcitx5
95 changes: 94 additions & 1 deletion src/config/about.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import Carbon
import SwiftUI

let sourceRepo = "https://github.com/fcitx-contrib/fcitx5-macos"
let uninstallLog = "/tmp/Fcitx5Uninstall.log"

func getDate() -> String {
let dateFormatter = DateFormatter()
Expand All @@ -25,7 +27,29 @@ func urlButton(_ text: String, _ link: String) -> some View {
.focusable(false)
}

let bundleId = "org.fcitx.inputmethod.Fcitx5"
let inputSourceId = bundleId

func disableInputMethod() {
let conditions = NSMutableDictionary()
conditions.setValue(bundleId, forKey: kTISPropertyBundleID as String)
// There are 2 items with kTISPropertyBundleID.
// We disable the parent, which has kTISPropertyInputSourceID: org.fcitx.inputmethod.Fcitx5
conditions.setValue(inputSourceId, forKey: kTISPropertyInputSourceID as String)
if let array = TISCreateInputSourceList(conditions, true)?.takeRetainedValue()
as? [TISInputSource]
{
for inputSource in array {
TISDisableInputSource(inputSource)
}
}
}

struct AboutView: View {
@State private var confirmUninstall = false
@State private var removeUserData = false
@State private var uninstallFailed = false

var body: some View {
VStack {
if let iconURL = Bundle.main.url(forResource: "fcitx", withExtension: "icns"),
Expand Down Expand Up @@ -65,14 +89,83 @@ struct AboutView: View {
urlButton(
NSLocalizedString("3rd-party source code", comment: ""),
sourceRepo + "/blob/master/README.md#credits")

Spacer().frame(height: gapSize)
HStack {
Button(
action: {
confirmUninstall = true
},
label: {
Text("Uninstall")
}
).sheet(
isPresented: $confirmUninstall
) {
VStack {
Text("Are you sure to uninstall?")
Button(
action: {
confirmUninstall = false
},
label: {
Text("Cancel")
})
Button(
action: {
removeUserData = false
uninstall()
},
label: {
Text("Uninstall and keep user data")
})
Button(
action: {
removeUserData = true
uninstall()
},
label: {
Text("Uninstall")
})
}.padding()
}.sheet(isPresented: $uninstallFailed) {
VStack {
Text("Uninstall failed, you may need to manually remove")
Text("/Library/Input Methods/Fcitx5.app")
Text("~/Library/fcitx5")
Text("~/.config/fcitx5")
if removeUserData {
Text("~/.local/share/fcitx5")
}
Button(
action: {
uninstallFailed = false
},
label: {
Text("OK")
}
).buttonStyle(.borderedProminent)
}.padding()
}
}
}.padding()
}

func uninstall() {
confirmUninstall = false
// It's necessary to disable not only for cleaning up.
// Without this, if user cancels sudo prompt and try again, UI will hang.
disableInputMethod()
if !sudo("uninstall", removeUserData ? "true" : "false", uninstallLog) {
uninstallFailed = true
}
}
}

class FcitxAbout: ConfigWindowController {
convenience init() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
contentRect: NSRect(x: 0, y: 0, width: 480, height: 600),
styleMask: [.titled, .closable],
backing: .buffered, defer: false)
window.contentView = NSHostingView(rootView: AboutView())
Expand Down
32 changes: 32 additions & 0 deletions src/config/sudo.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import Foundation
import Logging

func quote(_ s: String) -> String {
return s.replacingOccurrences(of: "\\", with: "\\\\").replacingOccurrences(of: "\"", with: "\\\"")
}

func twiceQuote(_ s: String) -> String {
let quoted = quote(s)
return quote("\"\(quoted)\"")
}

func sudo(_ script: String, _ arg: String, _ logPath: String) -> Bool {
let user = NSUserName()
guard let scriptPath = Bundle.main.path(forResource: script, ofType: "sh") else {
FCITX_ERROR("\(script).sh not found")
return false
}
let command =
"do shell script \"\(twiceQuote(scriptPath)) \(twiceQuote(user)) \(twiceQuote(arg)) 2>\(logPath)\" with administrator privileges"
guard let appleScript = NSAppleScript(source: command) else {
FCITX_ERROR("Fail to initialize AppleScript")
return false
}
var error: NSDictionary? = nil
appleScript.executeAndReturnError(&error)
if let error = error {
FCITX_ERROR("Fail to execute AppleScript: \(error)")
return false
}
return true
}

0 comments on commit 3a01146

Please sign in to comment.