1
1
mirror of https://github.com/qvacua/vimr.git synced 2024-12-28 08:13:17 +03:00
This commit is contained in:
Tae Won Ha 2018-07-09 17:59:15 +02:00
parent f9ce73625d
commit 634088a6bd
No known key found for this signature in database
GPG Key ID: E40743465B5B8B44
28 changed files with 1035 additions and 720 deletions

View File

@ -12,7 +12,8 @@
1929B05B9D664052EC2D23EF /* FileOutlineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BCE3E156C06EDF1F2806 /* FileOutlineView.swift */; };
1929B08C6230B9C5AB72DAF1 /* Pref128ToCurrentConverter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B5046239709E33516F5C /* Pref128ToCurrentConverter.swift */; };
1929B0E0C3BC59F52713D5A2 /* FoundationCommons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B9AF20D7BD6E5C975128 /* FoundationCommons.swift */; };
1929B0F599D1F62C7BE53D2C /* HttpServerService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B1DC584C89C477E83FA2 /* HttpServerService.swift */; };
1929B0F599D1F62C7BE53D2C /* HttpServerMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B1DC584C89C477E83FA2 /* HttpServerMiddleware.swift */; };
1929B10EE7FE8DC251B741B2 /* RxRedux.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B66A5E2D00EA143AFD86 /* RxRedux.swift */; };
1929B1837C750CADB3A5BCB9 /* OpenQuicklyFileViewRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B1558455B3A74D93EF2A /* OpenQuicklyFileViewRow.swift */; };
1929B20CE35B43BB1CE023BA /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BC2F05E9A5C0DB039739 /* Theme.swift */; };
1929B29B95AD176D57942E08 /* UiRootReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B457B9D0FA4D21F3751E /* UiRootReducer.swift */; };
@ -36,7 +37,7 @@
1929B5543B1E31A26096E656 /* FileMonitorReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B04EC69F616EEFAF5F96 /* FileMonitorReducer.swift */; };
1929B59FA5C286E010F70BEE /* Types.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BFC0A5A9C6DB09BE1368 /* Types.swift */; };
1929B5A2EE366F79ED32744C /* KeysPrefReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B88B5FA08E897A3C2168 /* KeysPrefReducer.swift */; };
1929B5C1BABBC0D09D97C3EF /* PreviewService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B617C229B19DB3E987B8 /* PreviewService.swift */; };
1929B5C1BABBC0D09D97C3EF /* PreviewMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B617C229B19DB3E987B8 /* PreviewMiddleware.swift */; };
1929B5F016431A76292D1E84 /* FileMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B365A6434354B568B04F /* FileMonitor.swift */; };
1929B6388EAF16C190B82955 /* FileItemIgnorePattern.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B69499B2569793350CEC /* FileItemIgnorePattern.swift */; };
1929B6460862447A31B5B082 /* ImageAndTextTableCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BDC3F82CB4CB4FE56D1B /* ImageAndTextTableCell.swift */; };
@ -54,13 +55,14 @@
1929B9318D32146D58BB38EC /* AppKitCommons.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B6A70931D60E04200E12030 /* AppKitCommons.swift */; };
1929B94083273D4B321AD848 /* FileItemUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B56C8ED31834BA9D8543 /* FileItemUtils.swift */; };
1929B98F94536E3912AD9F3B /* ArrayCommonsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BAF13FAD5DA8D3762367 /* ArrayCommonsTest.swift */; };
1929B990A143763A56CFCED0 /* PrefService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B364460D86F17E80943C /* PrefService.swift */; };
1929B990A143763A56CFCED0 /* PrefMiddleware.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B364460D86F17E80943C /* PrefMiddleware.swift */; };
1929BA715337FE26155B2071 /* BufferList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BA43449BA41666CD55ED /* BufferList.swift */; };
1929BA76A1D97D8226F7CFB1 /* Debouncer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B6AD3396160AA2C46919 /* Debouncer.swift */; };
1929BAAD7336FDFF1F78E749 /* ScorerTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BF69B01107F358CF7EAD /* ScorerTest.swift */; };
1929BAE4900D72A7877741B1 /* PrefWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BE168F31344B69E61B62 /* PrefWindow.swift */; };
1929BAFF1E011321D3186EE6 /* UiRoot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BD4149D5A25C82064DD8 /* UiRoot.swift */; };
1929BB4A9B2FA42A64CCCC76 /* MainWindowReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BD83A13BF133741766CC /* MainWindowReducer.swift */; };
1929BB67CAAD4F6CBD38DF0A /* RxRedux.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929B66A5E2D00EA143AFD86 /* RxRedux.swift */; };
1929BBE28654E4307AF1E2FD /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BC2F05E9A5C0DB039739 /* Theme.swift */; };
1929BCC7908DD899999B70BE /* AppearancePrefReducer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BED01F5D94BFCA4CF80F /* AppearancePrefReducer.swift */; };
1929BCC9D3604933DFF07E2E /* FileBrowser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1929BA5C7099CDEB04B76BA4 /* FileBrowser.swift */; };
@ -278,16 +280,17 @@
1929B12CE56A9B36980288A4 /* OpenQuicklyReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenQuicklyReducer.swift; sourceTree = "<group>"; };
1929B14A5949FB64C4B2646F /* KeysPref.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = KeysPref.swift; sourceTree = "<group>"; };
1929B1558455B3A74D93EF2A /* OpenQuicklyFileViewRow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenQuicklyFileViewRow.swift; sourceTree = "<group>"; };
1929B1DC584C89C477E83FA2 /* HttpServerService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpServerService.swift; sourceTree = "<group>"; };
1929B1DC584C89C477E83FA2 /* HttpServerMiddleware.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HttpServerMiddleware.swift; sourceTree = "<group>"; };
1929B34FC23D805A8B29E8F7 /* Context.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Context.swift; sourceTree = "<group>"; };
1929B364460D86F17E80943C /* PrefService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrefService.swift; sourceTree = "<group>"; };
1929B364460D86F17E80943C /* PrefMiddleware.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrefMiddleware.swift; sourceTree = "<group>"; };
1929B365A6434354B568B04F /* FileMonitor.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileMonitor.swift; sourceTree = "<group>"; };
1929B457B9D0FA4D21F3751E /* UiRootReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UiRootReducer.swift; sourceTree = "<group>"; };
1929B49E6924847AD085C8C9 /* PrefWindowReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrefWindowReducer.swift; sourceTree = "<group>"; };
1929B5046239709E33516F5C /* Pref128ToCurrentConverter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Pref128ToCurrentConverter.swift; sourceTree = "<group>"; };
1929B56C8ED31834BA9D8543 /* FileItemUtils.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileItemUtils.swift; sourceTree = "<group>"; };
1929B5D45C9792BBE76B8AFF /* StringCommonsTest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringCommonsTest.swift; sourceTree = "<group>"; };
1929B617C229B19DB3E987B8 /* PreviewService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewService.swift; sourceTree = "<group>"; };
1929B617C229B19DB3E987B8 /* PreviewMiddleware.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PreviewMiddleware.swift; sourceTree = "<group>"; };
1929B66A5E2D00EA143AFD86 /* RxRedux.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RxRedux.swift; sourceTree = "<group>"; };
1929B67A10E6BB2986B2416E /* BufferListReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = BufferListReducer.swift; sourceTree = "<group>"; };
1929B694508FB5FDE607513A /* ToolsPrefReducer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ToolsPrefReducer.swift; sourceTree = "<group>"; };
1929B69499B2569793350CEC /* FileItemIgnorePattern.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileItemIgnorePattern.swift; sourceTree = "<group>"; };
@ -565,7 +568,7 @@
1929B34FC23D805A8B29E8F7 /* Context.swift */,
1929B32401E8914DE9BF76CA /* Components */,
1929B5E773BDB3B4EE9D00C1 /* Reducers */,
1929BFA93DC859DD76C46192 /* Services */,
1929BFA93DC859DD76C46192 /* Middlewares */,
1929BFC0A5A9C6DB09BE1368 /* Types.swift */,
1929BA42AB6F1BF631B57399 /* SerializableStates.swift */,
);
@ -621,14 +624,14 @@
name = "Open Quickly";
sourceTree = "<group>";
};
1929BFA93DC859DD76C46192 /* Services */ = {
1929BFA93DC859DD76C46192 /* Middlewares */ = {
isa = PBXGroup;
children = (
1929B617C229B19DB3E987B8 /* PreviewService.swift */,
1929B1DC584C89C477E83FA2 /* HttpServerService.swift */,
1929B364460D86F17E80943C /* PrefService.swift */,
1929B617C229B19DB3E987B8 /* PreviewMiddleware.swift */,
1929B1DC584C89C477E83FA2 /* HttpServerMiddleware.swift */,
1929B364460D86F17E80943C /* PrefMiddleware.swift */,
);
name = Services;
name = Middlewares;
sourceTree = "<group>";
};
4B5012001EBA791000F76C46 /* Frameworks */ = {
@ -807,6 +810,7 @@
4B6423941D8EFD6100FC78C8 /* Workspace */,
4B97E2CF1D33F92200FC0660 /* resources */,
1929BA652D3B88FC071531EC /* UI */,
1929B66A5E2D00EA143AFD86 /* RxRedux.swift */,
);
path = VimR;
sourceTree = "<group>";
@ -1059,8 +1063,8 @@
4BE45C1D1FD2DBD2005C0A95 /* Logger.swift in Sources */,
1929B8FB248D71BF88A35761 /* PreviewTool.swift in Sources */,
1929B4B70926DE113E6BF990 /* PreviewReducer.swift in Sources */,
1929B5C1BABBC0D09D97C3EF /* PreviewService.swift in Sources */,
1929B0F599D1F62C7BE53D2C /* HttpServerService.swift in Sources */,
1929B5C1BABBC0D09D97C3EF /* PreviewMiddleware.swift in Sources */,
1929B0F599D1F62C7BE53D2C /* HttpServerMiddleware.swift in Sources */,
1929B3AC66EFE35D68C020E3 /* PreviewToolReducer.swift in Sources */,
1929B59FA5C286E010F70BEE /* Types.swift in Sources */,
1929B6D8F5FC723B7109031F /* OpenQuicklyReducer.swift in Sources */,
@ -1083,7 +1087,7 @@
1929B3557317755A43513B17 /* OpenQuicklyWindow.swift in Sources */,
1929B333855A5406C400DA92 /* OpenQuicklyFilterOperation.swift in Sources */,
1929B1837C750CADB3A5BCB9 /* OpenQuicklyFileViewRow.swift in Sources */,
1929B990A143763A56CFCED0 /* PrefService.swift in Sources */,
1929B990A143763A56CFCED0 /* PrefMiddleware.swift in Sources */,
1929BA76A1D97D8226F7CFB1 /* Debouncer.swift in Sources */,
1929B71610FF1DC6E459BA49 /* PreviewUtils.swift in Sources */,
1929B08C6230B9C5AB72DAF1 /* Pref128ToCurrentConverter.swift in Sources */,
@ -1103,6 +1107,7 @@
1929BE0F64A6CE5BCE2A5092 /* MainWindow+Delegates.swift in Sources */,
1929B8F498D1E7C53F572CE2 /* KeysPref.swift in Sources */,
1929B5A2EE366F79ED32744C /* KeysPrefReducer.swift in Sources */,
1929BB67CAAD4F6CBD38DF0A /* RxRedux.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -1130,6 +1135,7 @@
1929B8E90A1378E494D481E7 /* PrefUtilsTest.swift in Sources */,
1929B20CE35B43BB1CE023BA /* Theme.swift in Sources */,
1929B9318D32146D58BB38EC /* AppKitCommons.swift in Sources */,
1929B10EE7FE8DC251B741B2 /* RxRedux.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@ -5,11 +5,12 @@
import Foundation
class AdvancedPrefReducer {
class AdvancedPrefReducer: ReducerType {
typealias Pair = StateActionPair<AppState, AdvancedPref.Action>
typealias StateType = AppState
typealias ActionType = AdvancedPref.Action
func reduce(_ pair: Pair) -> Pair {
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var state = pair.state
switch pair.action {
@ -29,6 +30,6 @@ class AdvancedPrefReducer {
state.useSnapshotUpdate = value
}
return StateActionPair(state: state, action: pair.action)
return (state, pair.action, true)
}
}

View File

@ -35,10 +35,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
let baseServerUrl = URL(string: "http://localhost:\(NetUtils.openPort())")!
var initialAppState: AppState
if let stateDict = UserDefaults.standard.value(forKey: PrefService.compatibleVersion) as? [String: Any] {
if let stateDict = UserDefaults.standard.value(forKey: PrefMiddleware.compatibleVersion) as? [String: Any] {
initialAppState = AppState(dict: stateDict) ?? .default
} else {
if let oldDict = UserDefaults.standard.value(forKey: PrefService.lastCompatibleVersion) as? [String: Any] {
if let oldDict = UserDefaults.standard.value(forKey: PrefMiddleware.lastCompatibleVersion) as? [String: Any] {
initialAppState = Pref128ToCurrentConverter.appState(from: oldDict)
} else {
initialAppState = .default
@ -48,15 +48,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
baseServerUrl.appendingPathComponent(HtmlPreviewToolReducer.selectFirstPath)
)
self.stateContext = Context(baseServerUrl: baseServerUrl, state: initialAppState)
self.emit = self.stateContext.actionEmitter.typedEmit()
self.context = Context(baseServerUrl: baseServerUrl, state: initialAppState)
self.emit = self.context.actionEmitter.typedEmit()
self.openNewMainWindowOnLaunch = initialAppState.openNewMainWindowOnLaunch
self.openNewMainWindowOnReactivation = initialAppState.openNewMainWindowOnReactivation
self.useSnapshot = initialAppState.useSnapshotUpdate
let source = self.stateContext.stateSource
self.uiRoot = UiRoot(source: source, emitter: self.stateContext.actionEmitter, state: initialAppState)
let source = self.context.stateSource
self.uiRoot = UiRoot(source: source, emitter: self.context.actionEmitter, state: initialAppState)
super.init()
@ -101,7 +101,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserNotificationCenterDele
}
}
private let stateContext: Context
private let context: Context
private let emit: (Action) -> Void
private let uiRoot: UiRoot
@ -168,7 +168,7 @@ extension AppDelegate {
}
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
self.stateContext.savePrefs()
self.context.savePrefs()
if self.hasDirtyWindows && self.hasMainWindows {
let alert = NSAlert()

View File

@ -5,15 +5,16 @@
import Foundation
class AppDelegateReducer {
class AppDelegateReducer: ReducerType {
typealias Pair = StateActionPair<AppState, AppDelegate.Action>
typealias StateType = AppState
typealias ActionType = AppDelegate.Action
init(baseServerUrl: URL) {
self.baseServerUrl = baseServerUrl
}
func reduce(_ pair: Pair) -> Pair {
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var state = pair.state
switch pair.action {
@ -40,7 +41,7 @@ class AppDelegateReducer {
}
return StateActionPair(state: state, action: pair.action)
return (state, pair.action, true)
}
private let baseServerUrl: URL

View File

@ -5,11 +5,12 @@
import Foundation
class AppearancePrefReducer {
class AppearancePrefReducer: ReducerType {
typealias Pair = StateActionPair<AppState, AppearancePref.Action>
typealias StateType = AppState
typealias ActionType = AppearancePref.Action
func reduce(_ pair: Pair) -> Pair {
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var state = pair.state
var appearance = state.mainWindowTemplate.appearance
@ -34,7 +35,7 @@ class AppearancePrefReducer {
self.modify(state: &state, with: appearance)
return StateActionPair(state: state, action: pair.action)
return (state, pair.action, true)
}
private func modify(state: inout AppState, with appearance: AppearanceState) {

View File

@ -5,20 +5,21 @@
import Foundation
class BuffersListReducer {
class BuffersListReducer: ReducerType {
typealias Pair = StateActionPair<UuidState<MainWindow.State>, BuffersList.Action>
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<BuffersList.Action>
func reduce(_ pair: Pair) -> Pair {
var state = pair.state.payload
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var state = tuple.state
switch pair.action {
switch tuple.action.payload {
case let .open(buffer):
state.currentBufferToSet = buffer
}
return StateActionPair(state: UuidState(uuid: pair.state.uuid, state: state), action: pair.action)
return (state, tuple.action, true)
}
}

View File

@ -6,179 +6,120 @@
import Foundation
import RxSwift
class Context {
typealias AnyAction = Any
extension ReduxTypes {
let stateSource: Observable<AppState>
let actionEmitter = ActionEmitter()
typealias StateType = AppState
typealias ActionType = AnyAction
}
class Context: ReduxContext {
// The following should only be used when Cmd-Q'ing
func savePrefs() {
self.prefService.applyPref(from: self.appState)
self.prefMiddleware.applyPref(from: self.state)
}
init(baseServerUrl: URL, state: AppState) {
self.appState = state
self.stateSource = self.stateSubject.asObservable()
super.init(initialState: state)
let openQuicklyReducer = OpenQuicklyReducer()
let previewMiddleware = PreviewMiddleware()
let markdownReducer = MarkdownReducer(baseServerUrl: baseServerUrl)
let previewService = PreviewService()
let httpService: HttpServerService = HttpServerService(port: baseServerUrl.port!)
let httpMiddleware: HttpServerMiddleware = HttpServerMiddleware(port: baseServerUrl.port!)
let uiRootReducer = UiRootReducer()
let openQuicklyReducer = OpenQuicklyReducer()
// AppState
Observable
.of(
self.actionSourceForAppState()
.reduce(by: AppDelegateReducer(baseServerUrl: baseServerUrl).reduce)
.filterMapPair(),
self.actionSourceForAppState()
.reduce(by: uiRootReducer.reduceMainWindow)
.reduce(by: openQuicklyReducer.reduceMainWindow)
.filter { $0.modified }
.apply(self.prefService.applyMainWindow)
.map { $0.state },
self.actionSourceForAppState()
.reduce(by: FileMonitorReducer().reduce)
.filterMapPair(),
self.actionSourceForAppState()
.reduce(by: openQuicklyReducer.reduceOpenQuicklyWindow)
.filterMapPair(),
self.actionSourceForAppState()
.reduce(by: uiRootReducer.reduceUiRoot)
.filterMapPair()
)
.merge()
self.actionEmitter.observable
.map { (state: self.state, action: $0, modified: false) }
.reduce(
by: [
AppDelegateReducer(baseServerUrl: baseServerUrl).reduce,
uiRootReducer.mainWindow.reduce,
openQuicklyReducer.mainWindow.reduce,
FileMonitorReducer().reduce,
openQuicklyReducer.reduce,
uiRootReducer.reduce,
// Preferences
PrefWindowReducer().reduce,
GeneralPrefReducer().reduce,
ToolsPrefReducer().reduce,
AppearancePrefReducer().reduce,
AdvancedPrefReducer().reduce,
KeysPrefReducer().reduce,
],
middlewares: [
self.prefMiddleware.mainWindow.apply,
self.prefMiddleware.apply,
])
.filter { $0.modified }
.subscribe(onNext: self.emitAppState)
.disposed(by: self.disposeBag)
// MainWindow.State
Observable
.of(
self.actionSourceForMainWindow()
.reduce(by: MainWindowReducer().reduce)
.reduce(by: markdownReducer.reduceMainWindow)
.filter { $0.modified }
.apply(previewService.applyMainWindow)
.apply(httpService.applyMainWindow)
.map { $0.state },
self.actionSourceForMainWindow()
.reduce(by: markdownReducer.reducePreviewTool)
.reduce(by: PreviewToolReducer(baseServerUrl: baseServerUrl).reduce)
.filter { $0.modified }
.apply(previewService.applyPreviewTool)
.map { $0.state },
self.actionSourceForMainWindow()
.reduce(by: HtmlPreviewToolReducer(baseServerUrl: baseServerUrl).reduce)
.filter { $0.modified }
.apply(httpService.applyHtmlPreview)
.map { $0.state },
self.actionSourceForMainWindow()
.reduce(by: FileBrowserReducer().reduce)
.filterMapPair(),
self.actionSourceForMainWindow()
.reduce(by: BuffersListReducer().reduce)
.reduce(by: markdownReducer.reduceBufferList)
.filter { $0.modified }
.apply(previewService.applyBufferList)
.map { $0.state }
self.actionEmitter.observable
.mapOmittingNil { action in
guard let uuidAction = action as? UuidTagged else {
return nil
}
guard let mainWindowState = self.state.mainWindows[uuidAction.uuid] else {
return nil
}
return (mainWindowState, action, false)
}
.reduce(
by: [
MainWindowReducer().reduce,
markdownReducer.mainWindow.reduce,
markdownReducer.previewTool.reduce,
PreviewToolReducer(baseServerUrl: baseServerUrl).reduce,
HtmlPreviewToolReducer(baseServerUrl: baseServerUrl).reduce,
FileBrowserReducer().reduce,
BuffersListReducer().reduce,
markdownReducer.buffersList.reduce,
],
middlewares: [
previewMiddleware.mainWindow.apply,
httpMiddleware.mainWindow.apply,
previewMiddleware.previewTool.apply,
httpMiddleware.htmlPreview.apply,
previewMiddleware.buffersList.apply,
]
)
.merge()
.filter { $0.modified }
.subscribe(onNext: self.emitAppState)
.disposed(by: self.disposeBag)
// Preferences
Observable
.of(
self.prefStateSource(by: PrefWindowReducer().reduce, prefService: prefService),
self.prefStateSource(by: GeneralPrefReducer().reduce, prefService: prefService),
self.prefStateSource(by: ToolsPrefReducer().reduce, prefService: prefService),
self.prefStateSource(by: AppearancePrefReducer().reduce, prefService: prefService),
self.prefStateSource(by: AdvancedPrefReducer().reduce, prefService: prefService),
self.prefStateSource(by: KeysPrefReducer().reduce, prefService: prefService)
)
.merge()
.subscribe(onNext: self.emitAppState)
.disposed(by: self.disposeBag)
#if DEBUG
// self.actionEmitter.observable.debug().subscribe().disposed(by: self.disposeBag)
// stateSource.debug().subscribe().disposed(by: self.disposeBag)
#endif
}
deinit {
self.stateSubject.onCompleted()
}
private let prefMiddleware = PrefMiddleware()
private let stateSubject = PublishSubject<AppState>()
private let scheduler = SerialDispatchQueueScheduler(qos: .userInitiated)
private let disposeBag = DisposeBag()
private func emitAppState(_ tuple: (state: MainWindow.State, action: AnyAction, modified: Bool)) {
guard let uuidAction = tuple.action as? UuidTagged else {
return
}
private var appState: AppState
private let prefService = PrefService()
private func emitAppState(_ mainWindow: UuidState<MainWindow.State>) {
self.appState.mainWindows[mainWindow.uuid] = mainWindow.payload
self.stateSubject.onNext(self.appState)
self.state.mainWindows[uuidAction.uuid] = tuple.state
self.stateSubject.onNext(self.state)
self.cleanUpAppState()
}
private func emitAppState(_ appState: AppState) {
self.appState = appState
self.stateSubject.onNext(self.appState)
private func emitAppState(_ tuple: ReduxTypes.ReduceTuple) {
self.state = tuple.state
self.stateSubject.onNext(self.state)
self.cleanUpAppState()
}
private func cleanUpAppState() {
self.appState.mainWindows.keys.forEach { uuid in
self.appState.mainWindows[uuid]?.cwdToSet = nil
self.appState.mainWindows[uuid]?.currentBufferToSet = nil
self.appState.mainWindows[uuid]?.viewToBeFocused = nil
self.appState.mainWindows[uuid]?.urlsToOpen.removeAll()
self.state.mainWindows.keys.forEach { uuid in
self.state.mainWindows[uuid]?.cwdToSet = nil
self.state.mainWindows[uuid]?.currentBufferToSet = nil
self.state.mainWindows[uuid]?.viewToBeFocused = nil
self.state.mainWindows[uuid]?.urlsToOpen.removeAll()
}
}
private func actionSourceForAppState<ActionType>() -> Observable<StateActionPair<AppState, ActionType>> {
return self.actionEmitter.observable
.mapOmittingNil { $0 as? ActionType }
.map { self.appStateActionPair(for: $0) }
}
private func actionSourceForMainWindow<ActionType>()
-> Observable<StateActionPair<UuidState<MainWindow.State>, ActionType>> {
return self.actionEmitter.observable
.mapOmittingNil { $0 as? UuidAction<ActionType> }
.mapOmittingNil { self.mainWindowStateActionPair(for: $0) }
}
private func prefStateSource<ActionType>(
by reduce: @escaping (StateActionPair<AppState, ActionType>) -> StateActionPair<AppState, ActionType>,
prefService: PrefService
) -> Observable<AppState> {
return self.actionSourceForAppState()
.reduce(by: reduce)
.filter { $0.modified }
.apply(self.prefService.applyPref)
.map { $0.state }
}
private func appStateActionPair<ActionType>(for action: ActionType) -> StateActionPair<AppState, ActionType> {
return StateActionPair(state: self.appState, action: action, modified: false)
}
private func mainWindowStateActionPair<ActionType>(for action: UuidAction<ActionType>)
-> StateActionPair<UuidState<MainWindow.State>, ActionType>? {
guard let mainWindowState = self.appState.mainWindows[action.uuid] else {
return nil
}
return StateActionPair(state: UuidState(uuid: action.uuid, state: mainWindowState),
action: action.payload,
modified: false)
}
}

View File

@ -5,14 +5,15 @@
import Foundation
class FileBrowserReducer {
class FileBrowserReducer: ReducerType {
typealias Pair = StateActionPair<UuidState<MainWindow.State>, FileBrowser.Action>
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<FileBrowser.Action>
func reduce(_ pair: Pair) -> Pair {
var state = pair.state.payload
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var state = tuple.state
switch pair.action {
switch tuple.action.payload {
case let .open(url, mode):
state.urlsToOpen[url] = mode
@ -29,6 +30,6 @@ class FileBrowserReducer {
}
return StateActionPair(state: UuidState(uuid: state.uuid, state: state), action: pair.action)
return (state, tuple.action, true)
}
}

View File

@ -5,14 +5,15 @@
import Foundation
class FileMonitorReducer {
class FileMonitorReducer: ReducerType {
typealias Pair = StateActionPair<AppState, FileMonitor.Action>
typealias StateType = AppState
typealias ActionType = FileMonitor.Action
func reduce(_ pair: Pair) -> Pair {
var state = pair.state
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var state = tuple.state
switch pair.action {
switch tuple.action {
case let .change(in: url):
if let fileItem = FileItemUtils.item(for: url, root: state.openQuickly.root, create: false) {
@ -28,6 +29,6 @@ class FileMonitorReducer {
}
return StateActionPair(state: state, action: pair.action)
return (state, tuple.action, true)
}
}

View File

@ -5,11 +5,12 @@
import Foundation
class GeneralPrefReducer {
class GeneralPrefReducer: ReducerType {
typealias Pair = StateActionPair<AppState, GeneralPref.Action>
typealias StateType = AppState
typealias ActionType = GeneralPref.Action
func reduce(_ pair: Pair) -> Pair {
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var state = pair.state
switch pair.action {
@ -29,6 +30,6 @@ class GeneralPrefReducer {
}
return StateActionPair(state: state, action: pair.action)
return (state, pair.action, true)
}
}

View File

@ -5,9 +5,10 @@
import Foundation
class HtmlPreviewToolReducer {
class HtmlPreviewToolReducer: ReducerType {
typealias Pair = StateActionPair<UuidState<MainWindow.State>, HtmlPreviewTool.Action>
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<HtmlPreviewTool.Action>
static let basePath = "/tools/html-preview"
static let selectFirstPath = "/tools/html-preview/select-first.html"
@ -16,11 +17,11 @@ class HtmlPreviewToolReducer {
self.baseServerUrl = baseServerUrl
}
func reduce(_ pair: Pair) -> Pair {
var state = pair.state.payload
let uuid = pair.state.uuid
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var state = pair.state
let uuid = pair.action.uuid
switch pair.action {
switch pair.action.payload {
case let .selectHtmlFile(url):
state.htmlPreview.htmlFile = url
@ -30,7 +31,7 @@ class HtmlPreviewToolReducer {
}
return StateActionPair(state: UuidState(uuid: state.uuid, state: state), action: pair.action)
return (state, pair.action, true)
}
private let baseServerUrl: URL

View File

@ -0,0 +1,121 @@
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Foundation
import Swifter
class HttpServerMiddleware {
let htmlPreview: HtmlPreviewMiddleware
let mainWindow: MainWindowMiddleware
init(port: Int) {
let server = HttpServer()
let resourceUrl = Bundle.main.resourceURL!
let githubCssUrl = resourceUrl.appendingPathComponent("markdown/github-markdown.css")
self.htmlPreview = HtmlPreviewMiddleware(server: server, githubCssUrl: githubCssUrl)
self.mainWindow = MainWindowMiddleware(server: server, githubCssUrl: githubCssUrl)
do {
try server.start(in_port_t(port))
stdoutLog.info("VimR http server started on http://localhost:\(port)")
let previewResourceUrl = resourceUrl.appendingPathComponent("preview")
server["\(MarkdownReducer.basePath)/:path"] = shareFilesFromDirectory(previewResourceUrl.path)
server.GET["\(MarkdownReducer.basePath)/github-markdown.css"] = shareFile(githubCssUrl.path)
server["\(HtmlPreviewToolReducer.basePath)/:path"] = shareFilesFromDirectory(previewResourceUrl.path)
server.GET["\(HtmlPreviewToolReducer.basePath)/github-markdown.css"] = shareFile(githubCssUrl.path)
} catch {
stdoutLog.error("Server could not be started on port \(port): \(error)")
}
}
class HtmlPreviewMiddleware: MiddlewareType {
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<HtmlPreviewTool.Action>
init(server: HttpServer, githubCssUrl: URL) {
self.server = server
self.githubCssUrl = githubCssUrl
}
func typedApply(_ reduce: @escaping TypedActionReduceFunction) -> TypedActionReduceFunction {
return { tuple in
let result = reduce(tuple)
guard tuple.modified else {
return result
}
let state = result.state
guard let serverUrl = state.htmlPreview.server, let htmlFileUrl = state.htmlPreview.htmlFile else {
return result
}
let basePath = serverUrl.payload.deletingLastPathComponent().path
self.server.GET[serverUrl.payload.path] = shareFile(htmlFileUrl.path)
self.server["\(basePath)/:path"] = shareFilesFromDirectory(htmlFileUrl.parent.path)
return result
}
}
private let server: HttpServer
private let githubCssUrl: URL
}
class MainWindowMiddleware: MiddlewareType {
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<MainWindow.Action>
init(server: HttpServer, githubCssUrl: URL) {
self.server = server
self.githubCssUrl = githubCssUrl
}
func typedApply(_ reduce: @escaping TypedActionReduceFunction) -> TypedActionReduceFunction {
return { tuple in
let result = reduce(tuple)
guard tuple.modified else {
return result
}
let uuidAction = tuple.action
guard case .newCurrentBuffer = uuidAction.payload else {
return result
}
let preview = result.state.preview
guard case .markdown = preview.status,
let buffer = preview.buffer,
let html = preview.html,
let server = preview.server else {
return result
}
fileLog.debug("Serving \(html) on \(server)")
let htmlBasePath = server.deletingLastPathComponent().path
self.server["\(htmlBasePath)/:path"] = shareFilesFromDirectory(buffer.deletingLastPathComponent().path)
self.server.GET[server.path] = shareFile(html.path)
self.server.GET["\(htmlBasePath)/github-markdown.css"] = shareFile(self.githubCssUrl.path)
return result
}
}
private let server: HttpServer
private let githubCssUrl: URL
}
}

View File

@ -1,72 +0,0 @@
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Foundation
import Swifter
class HttpServerService {
typealias HtmlPreviewPair = StateActionPair<UuidState<MainWindow.State>, HtmlPreviewTool.Action>
typealias MainWindowPair = StateActionPair<UuidState<MainWindow.State>, MainWindow.Action>
init(port: Int) {
let resourceUrl = Bundle.main.resourceURL!
self.githubCssUrl = resourceUrl.appendingPathComponent("markdown/github-markdown.css")
do {
try self.server.start(in_port_t(port))
stdoutLog.info("VimR http server started on http://localhost:\(port)")
let previewResourceUrl = resourceUrl.appendingPathComponent("preview")
self.server["\(MarkdownReducer.basePath)/:path"] = shareFilesFromDirectory(previewResourceUrl.path)
self.server.GET["\(MarkdownReducer.basePath)/github-markdown.css"] = shareFile(githubCssUrl.path)
self.server["\(HtmlPreviewToolReducer.basePath)/:path"] = shareFilesFromDirectory(previewResourceUrl.path)
self.server.GET["\(HtmlPreviewToolReducer.basePath)/github-markdown.css"] = shareFile(githubCssUrl.path)
} catch {
NSLog("ERROR server could not be started on port \(port)")
}
}
func applyHtmlPreview(_ pair: HtmlPreviewPair) {
let state = pair.state.payload
guard let serverUrl = state.htmlPreview.server, let htmlFileUrl = state.htmlPreview.htmlFile else {
return
}
let basePath = serverUrl.payload.deletingLastPathComponent().path
self.server.GET[serverUrl.payload.path] = shareFile(htmlFileUrl.path)
self.server["\(basePath)/:path"] = shareFilesFromDirectory(htmlFileUrl.parent.path)
}
func applyMainWindow(_ pair: MainWindowPair) {
guard case .newCurrentBuffer = pair.action else {
return
}
let preview = pair.state.payload.preview
guard case .markdown = preview.status,
let buffer = preview.buffer,
let html = preview.html,
let server = preview.server
else {
return
}
fileLog.debug("Serving \(html) on \(server)")
let htmlBasePath = server.deletingLastPathComponent().path
self.server["\(htmlBasePath)/:path"] = shareFilesFromDirectory(buffer.deletingLastPathComponent().path)
self.server.GET[server.path] = shareFile(html.path)
self.server.GET["\(htmlBasePath)/github-markdown.css"] = shareFile(self.githubCssUrl.path)
}
private let server = HttpServer()
private let githubCssUrl: URL
}

View File

@ -5,11 +5,12 @@
import Foundation
class KeysPrefReducer {
class KeysPrefReducer: ReducerType {
typealias Pair = StateActionPair<AppState, KeysPref.Action>
typealias StateType = AppState
typealias ActionType = KeysPref.Action
func reduce(_ pair: Pair) -> Pair {
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var state = pair.state
switch pair.action {
@ -24,6 +25,6 @@ class KeysPrefReducer {
}
return StateActionPair(state: state, action: pair.action)
return (state, pair.action, true)
}
}

View File

@ -5,14 +5,15 @@
import Foundation
class MainWindowReducer {
class MainWindowReducer: ReducerType {
typealias Pair = StateActionPair<UuidState<MainWindow.State>, MainWindow.Action>
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<MainWindow.Action>
func reduce(_ pair: Pair) -> Pair {
var state = pair.state.payload
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var state = tuple.state
switch pair.action {
switch tuple.action.payload {
case let .frameChanged(to:frame):
state.frame = frame
@ -30,8 +31,8 @@ class MainWindowReducer {
case let .setDirtyStatus(status):
// When I gt or w around, we change tab somehow... Dunno why...
if status == pair.state.payload.isDirty {
return pair
if status == tuple.state.isDirty {
return tuple
}
state.isDirty = status
@ -78,10 +79,10 @@ class MainWindowReducer {
state.appearance.theme = Marked(theme)
default:
return pair
return tuple
}
return StateActionPair(state: UuidState(uuid: state.uuid, state: state), action: pair.action)
return (state, tuple.action, true)
}
}

View File

@ -6,12 +6,14 @@
import Foundation
import RxSwift
class OpenQuicklyReducer {
class OpenQuicklyReducer: ReducerType {
typealias OpenQuicklyWindowPair = StateActionPair<AppState, OpenQuicklyWindow.Action>
typealias MainWindowPair = StateActionPair<AppState, UuidAction<MainWindow.Action>>
typealias StateType = AppState
typealias ActionType = OpenQuicklyWindow.Action
func reduceOpenQuicklyWindow(_ pair: OpenQuicklyWindowPair) -> OpenQuicklyWindowPair {
let mainWindow = MainWindowReducer()
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var appState = pair.state
appState.openQuickly.open = false
@ -32,31 +34,37 @@ class OpenQuicklyReducer {
}
return StateActionPair(state: appState, action: pair.action)
return (appState, pair.action, true)
}
func reduceMainWindow(_ pair: MainWindowPair) -> MainWindowPair {
switch pair.action.payload {
class MainWindowReducer: ReducerType {
case .openQuickly:
var appState = pair.state
typealias StateType = AppState
typealias ActionType = UuidAction<MainWindow.Action>
guard let uuid = appState.currentMainWindowUuid else {
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
switch pair.action.payload {
case .openQuickly:
var appState = pair.state
guard let uuid = appState.currentMainWindowUuid else {
return pair
}
guard let cwd = appState.mainWindows[uuid]?.cwd else {
return pair
}
appState.openQuickly.open = true
appState.openQuickly.cwd = cwd
return (appState, pair.action, true)
default:
return pair
}
guard let cwd = appState.mainWindows[uuid]?.cwd else {
return pair
}
appState.openQuickly.open = true
appState.openQuickly.cwd = cwd
return StateActionPair(state: appState, action: pair.action)
default:
return pair
}
}
}

View File

@ -0,0 +1,62 @@
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Foundation
class PrefMiddleware: MiddlewareType {
typealias StateType = AppState
typealias ActionType = AnyAction
static let compatibleVersion = "168"
static let lastCompatibleVersion = "128"
let mainWindow = MainWindowMiddleware()
// The following should only be used when Cmd-Q'ing
func applyPref(from appState: AppState) {
defaults.setValue(appState.dict(), forKey: PrefMiddleware.compatibleVersion)
}
func typedApply(_ reduce: @escaping TypedActionReduceFunction) -> TypedActionReduceFunction {
return { tuple in
let result = reduce(tuple)
guard tuple.modified else {
return result
}
defaults.setValue(result.state.dict(), forKey: PrefMiddleware.compatibleVersion)
return result
}
}
class MainWindowMiddleware: MiddlewareType {
typealias StateType = AppState
typealias ActionType = UuidAction<MainWindow.Action>
func typedApply(_ reduce: @escaping TypedActionReduceFunction) -> TypedActionReduceFunction {
return { tuple in
let result = reduce(tuple)
guard tuple.modified else {
return result
}
let uuidAction = tuple.action
guard case .close = uuidAction.payload else {
return result
}
defaults.setValue(result.state.dict(), forKey: PrefMiddleware.compatibleVersion)
return result
}
}
}
}
private let defaults = UserDefaults.standard

View File

@ -1,33 +0,0 @@
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Foundation
private let defaults = UserDefaults.standard
class PrefService {
typealias MainWindowPair = StateActionPair<AppState, UuidAction<MainWindow.Action>>
static let compatibleVersion = "168"
static let lastCompatibleVersion = "128"
// The following should only be used when Cmd-Q'ing
func applyPref(from appState: AppState) {
defaults.setValue(appState.dict(), forKey: PrefService.compatibleVersion)
}
func applyPref<ActionType>(_ pair: StateActionPair<AppState, ActionType>) {
defaults.setValue(pair.state.dict(), forKey: PrefService.compatibleVersion)
}
func applyMainWindow(_ pair: MainWindowPair) {
guard case .close = pair.action.payload else {
return
}
defaults.setValue(pair.state.dict(), forKey: PrefService.compatibleVersion)
}
}

View File

@ -5,11 +5,12 @@
import Foundation
class PrefWindowReducer {
class PrefWindowReducer: ReducerType {
typealias Pair = StateActionPair<AppState, PrefWindow.Action>
typealias StateType = AppState
typealias ActionType = PrefWindow.Action
func reduce(_ pair: Pair) -> Pair {
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var state = pair.state
switch pair.action {
@ -19,6 +20,6 @@ class PrefWindowReducer {
}
return StateActionPair(state: state, action: pair.action)
return (state, pair.action, true)
}
}

View File

@ -0,0 +1,186 @@
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Foundation
import CocoaMarkdown
class PreviewMiddleware {
let previewTool: PreviewToolMiddleware
let buffersList: BuffersListMiddleware
let mainWindow: MainWindowMiddleware
init() {
let generator = PreviewGenerator()
self.previewTool = PreviewToolMiddleware(generator: generator)
self.buffersList = BuffersListMiddleware(generator: generator)
self.mainWindow = MainWindowMiddleware(generator: generator)
}
class PreviewGenerator {
init() {
guard let templateUrl = Bundle.main.url(forResource: "template",
withExtension: "html",
subdirectory: "markdown")
else {
preconditionFailure("ERROR Cannot load markdown template")
}
guard let template = try? String(contentsOf: templateUrl) else {
preconditionFailure("ERROR Cannot load markdown template")
}
self.template = template
}
func apply(_ state: MainWindow.State, uuid: String) {
let preview = state.preview
guard let buffer = preview.buffer, let html = preview.html else {
guard let previewUrl = self.previewFiles[uuid] else {
return
}
try? FileManager.default.removeItem(at: previewUrl)
self.previewFiles.removeValue(forKey: uuid)
return
}
fileLog.debug("\(buffer) -> \(html)")
do {
try self.render(buffer, to: html)
self.previewFiles[uuid] = html
} catch let error as NSError {
// FIXME: error handling!
NSLog("ERROR rendering \(buffer) to \(html): \(error)")
return
}
}
private func render(_ bufferUrl: URL, to htmlUrl: URL) throws {
let doc = CMDocument(contentsOfFile: bufferUrl.path, options: .sourcepos)
let renderer = CMHTMLRenderer(document: doc)
guard let body = renderer?.render() else {
// FIXME: error handling!
return
}
let html = filledTemplate(body: body, title: bufferUrl.lastPathComponent)
let htmlFilePath = htmlUrl.path
try html.write(toFile: htmlFilePath, atomically: true, encoding: .utf8)
}
private func filledTemplate(body: String, title: String) -> String {
return self.template
.replacingOccurrences(of: "{{ title }}", with: title)
.replacingOccurrences(of: "{{ body }}", with: body)
}
private let template: String
private var previewFiles = [String: URL]()
}
class PreviewToolMiddleware: MiddlewareType {
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<PreviewTool.Action>
init(generator: PreviewGenerator) {
self.generator = generator
}
func typedApply(_ reduce: @escaping TypedActionReduceFunction) -> TypedActionReduceFunction {
return { tuple in
let result = reduce(tuple)
guard tuple.modified else {
return result
}
let uuidAction = tuple.action
guard case .refreshNow = uuidAction.payload else {
return result
}
self.generator.apply(result.state, uuid: uuidAction.uuid)
return result
}
}
private let generator: PreviewGenerator
}
class BuffersListMiddleware: MiddlewareType {
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<BuffersList.Action>
init(generator: PreviewGenerator) {
self.generator = generator
}
func typedApply(_ reduce: @escaping TypedActionReduceFunction) -> TypedActionReduceFunction {
return { tuple in
let result = reduce(tuple)
guard tuple.modified else {
return result
}
let uuidAction = tuple.action
guard case .open = uuidAction.payload else {
return result
}
self.generator.apply(result.state, uuid: uuidAction.uuid)
return result
}
}
private let generator: PreviewGenerator
}
class MainWindowMiddleware: MiddlewareType {
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<MainWindow.Action>
init(generator: PreviewGenerator) {
self.generator = generator
}
func typedApply(_ reduce: @escaping TypedActionReduceFunction) -> TypedActionReduceFunction {
return { tuple in
let result = reduce(tuple)
guard tuple.modified else {
return result
}
let uuidAction = tuple.action
switch uuidAction.payload {
case .newCurrentBuffer:
self.generator.apply(result.state, uuid: uuidAction.uuid)
case .bufferWritten:
self.generator.apply(result.state, uuid: uuidAction.uuid)
default:
return result
}
return result
}
}
private let generator: PreviewGenerator
}
}

View File

@ -7,124 +7,160 @@ import Foundation
class MarkdownReducer {
typealias PreviewToolPair = StateActionPair<UuidState<MainWindow.State>, PreviewTool.Action>
typealias BufferListPair = StateActionPair<UuidState<MainWindow.State>, BuffersList.Action>
typealias MainWindowPair = StateActionPair<UuidState<MainWindow.State>, MainWindow.Action>
static let basePath = "/tools/markdown"
static let saveFirstPath = "/tools/markdown/save-first.html"
static let errorPath = "/tools/markdown/error.html"
static let nonePath = "/tools/markdown/empty.html"
func reducePreviewTool(_ pair: PreviewToolPair) -> PreviewToolPair {
var state = pair.state.payload
switch pair.action {
case .refreshNow:
state.preview = PreviewUtils.state(for: pair.state.uuid,
baseUrl: self.baseServerUrl,
buffer: state.currentBuffer,
editorPosition: state.preview.editorPosition,
previewPosition: state.preview.previewPosition)
state.preview.lastSearch = .reload
case let .reverseSearch(to:position):
state.preview.previewPosition = Marked(position)
state.preview.lastSearch = .reverse
case let .scroll(to:position):
if state.preview.lastSearch == .reload {
state.preview.lastSearch = .none
break;
}
guard state.previewTool.isReverseSearchAutomatically && state.preview.lastSearch != .forward else {
state.preview.lastSearch = .none
state.preview.previewPosition = Marked(mark: state.preview.previewPosition.mark, payload: position)
break;
}
state.preview.previewPosition = Marked(position)
state.preview.lastSearch = .reverse
default:
return pair
}
return StateActionPair(state: UuidState(uuid: state.uuid, state: state), action: pair.action)
}
func reduceBufferList(_ pair: BufferListPair) -> BufferListPair {
var state = pair.state.payload
switch pair.action {
case let .open(buffer):
state.preview = PreviewUtils.state(for: pair.state.uuid,
baseUrl: self.baseServerUrl,
buffer: buffer,
editorPosition: Marked(.beginning),
previewPosition: Marked(.beginning))
state.preview.lastSearch = .none
}
return StateActionPair(state: UuidState(uuid: pair.state.uuid, state: state), action: pair.action)
}
func reduceMainWindow(_ pair: MainWindowPair) -> MainWindowPair {
var state = pair.state.payload
switch pair.action {
case let .newCurrentBuffer(buffer):
state.preview = PreviewUtils.state(for: pair.state.uuid, baseUrl: self.baseServerUrl, buffer: buffer,
editorPosition: state.preview.editorPosition,
previewPosition: state.preview.previewPosition)
state.preview.lastSearch = .none
case .bufferWritten:
state.preview = PreviewUtils.state(for: pair.state.uuid,
baseUrl: self.baseServerUrl,
buffer: state.currentBuffer,
editorPosition: state.preview.editorPosition,
previewPosition: state.preview.previewPosition)
state.preview.lastSearch = .reload
case let .setCursor(to:position):
if state.preview.lastSearch == .reload {
state.preview.lastSearch = .none
break
}
guard state.previewTool.isForwardSearchAutomatically && state.preview.lastSearch != .reverse else {
state.preview.editorPosition = Marked(mark: state.preview.editorPosition.mark, payload: position.payload)
state.preview.lastSearch = .none
break
}
state.preview.editorPosition = Marked(position.payload)
state.preview.lastSearch = .none // .none because the forward search does not invoke .scroll above.
case .close:
state.preview = PreviewUtils.state(for: .none,
baseUrl: self.baseServerUrl,
editorPosition: state.preview.editorPosition,
previewPosition: state.preview.previewPosition)
state.preview.lastSearch = .none
default:
return pair
}
return StateActionPair(state: UuidState(uuid: pair.state.uuid, state: state), action: pair.action)
}
let previewTool: PreviewToolReducer
let buffersList: BuffersListReducer
let mainWindow: MainWindowReducer
init(baseServerUrl: URL) {
self.baseServerUrl = baseServerUrl
self.previewTool = PreviewToolReducer(baseServerUrl: baseServerUrl)
self.buffersList = BuffersListReducer(baseServerUrl: baseServerUrl)
self.mainWindow = MainWindowReducer(baseServerUrl: baseServerUrl)
}
private let baseServerUrl: URL
class PreviewToolReducer: ReducerType {
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<PreviewTool.Action>
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var state = tuple.state
switch tuple.action.payload {
case .refreshNow:
state.preview = PreviewUtils.state(for: tuple.state.uuid,
baseUrl: self.baseServerUrl,
buffer: state.currentBuffer,
editorPosition: state.preview.editorPosition,
previewPosition: state.preview.previewPosition)
state.preview.lastSearch = .reload
case let .reverseSearch(to:position):
state.preview.previewPosition = Marked(position)
state.preview.lastSearch = .reverse
case let .scroll(to:position):
if state.preview.lastSearch == .reload {
state.preview.lastSearch = .none
break;
}
guard state.previewTool.isReverseSearchAutomatically && state.preview.lastSearch != .forward else {
state.preview.lastSearch = .none
state.preview.previewPosition = Marked(mark: state.preview.previewPosition.mark, payload: position)
break;
}
state.preview.previewPosition = Marked(position)
state.preview.lastSearch = .reverse
default:
return tuple
}
return (state, tuple.action, true)
}
init(baseServerUrl: URL) {
self.baseServerUrl = baseServerUrl
}
private let baseServerUrl: URL
}
class BuffersListReducer: ReducerType {
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<BuffersList.Action>
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var state = tuple.state
switch tuple.action.payload {
case let .open(buffer):
state.preview = PreviewUtils.state(for: tuple.state.uuid,
baseUrl: self.baseServerUrl,
buffer: buffer,
editorPosition: Marked(.beginning),
previewPosition: Marked(.beginning))
state.preview.lastSearch = .none
}
return (state, tuple.action, true)
}
init(baseServerUrl: URL) {
self.baseServerUrl = baseServerUrl
}
private let baseServerUrl: URL
}
class MainWindowReducer: ReducerType {
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<MainWindow.Action>
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var state = tuple.state
switch tuple.action.payload {
case let .newCurrentBuffer(buffer):
state.preview = PreviewUtils.state(for: tuple.state.uuid, baseUrl: self.baseServerUrl, buffer: buffer,
editorPosition: state.preview.editorPosition,
previewPosition: state.preview.previewPosition)
state.preview.lastSearch = .none
case .bufferWritten:
state.preview = PreviewUtils.state(for: tuple.state.uuid,
baseUrl: self.baseServerUrl,
buffer: state.currentBuffer,
editorPosition: state.preview.editorPosition,
previewPosition: state.preview.previewPosition)
state.preview.lastSearch = .reload
case let .setCursor(to:position):
if state.preview.lastSearch == .reload {
state.preview.lastSearch = .none
break
}
guard state.previewTool.isForwardSearchAutomatically && state.preview.lastSearch != .reverse else {
state.preview.editorPosition = Marked(mark: state.preview.editorPosition.mark, payload: position.payload)
state.preview.lastSearch = .none
break
}
state.preview.editorPosition = Marked(position.payload)
state.preview.lastSearch = .none // .none because the forward search does not invoke .scroll above.
case .close:
state.preview = PreviewUtils.state(for: .none,
baseUrl: self.baseServerUrl,
editorPosition: state.preview.editorPosition,
previewPosition: state.preview.previewPosition)
state.preview.lastSearch = .none
default:
return tuple
}
return (state, tuple.action, true)
}
init(baseServerUrl: URL) {
self.baseServerUrl = baseServerUrl
}
private let baseServerUrl: URL
}
}

View File

@ -1,104 +0,0 @@
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Foundation
import CocoaMarkdown
class PreviewService {
typealias PreviewToolPair = StateActionPair<UuidState<MainWindow.State>, PreviewTool.Action>
typealias BufferListPair = StateActionPair<UuidState<MainWindow.State>, BuffersList.Action>
typealias MainWindowPair = StateActionPair<UuidState<MainWindow.State>, MainWindow.Action>
init() {
guard let templateUrl = Bundle.main.url(forResource: "template",
withExtension: "html",
subdirectory: "markdown")
else {
preconditionFailure("ERROR Cannot load markdown template")
}
guard let template = try? String(contentsOf: templateUrl) else {
preconditionFailure("ERROR Cannot load markdown template")
}
self.template = template
}
func applyPreviewTool(_ pair: PreviewToolPair) {
guard case .refreshNow = pair.action else {
return
}
self.apply(pair.state)
}
func applyBufferList(_ pair: BufferListPair) {
guard case .open = pair.action else {
return
}
self.apply(pair.state)
}
func applyMainWindow(_ pair: MainWindowPair) {
switch pair.action {
case .newCurrentBuffer: self.apply(pair.state)
case .bufferWritten: self.apply(pair.state)
default: return
}
}
private func filledTemplate(body: String, title: String) -> String {
return self.template
.replacingOccurrences(of: "{{ title }}", with: title)
.replacingOccurrences(of: "{{ body }}", with: body)
}
private func render(_ bufferUrl: URL, to htmlUrl: URL) throws {
let doc = CMDocument(contentsOfFile: bufferUrl.path, options: .sourcepos)
let renderer = CMHTMLRenderer(document: doc)
guard let body = renderer?.render() else {
// FIXME: error handling!
return
}
let html = filledTemplate(body: body, title: bufferUrl.lastPathComponent)
let htmlFilePath = htmlUrl.path
try html.write(toFile: htmlFilePath, atomically: true, encoding: .utf8)
}
private func apply(_ state: UuidState<MainWindow.State>) {
let uuid = state.uuid
let preview = state.payload.preview
guard let buffer = preview.buffer, let html = preview.html else {
guard let previewUrl = self.previewFiles[uuid] else {
return
}
try? FileManager.default.removeItem(at: previewUrl)
self.previewFiles.removeValue(forKey: uuid)
return
}
// NSLog("\(buffer) -> \(html)")
do {
try self.render(buffer, to: html)
self.previewFiles[uuid] = html
} catch let error as NSError {
// FIXME: error handling!
NSLog("ERROR rendering \(buffer) to \(html): \(error)")
return
}
}
private let template: String
private let tempDir = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
private var previewFiles = [String: URL]()
}

View File

@ -5,18 +5,19 @@
import Foundation
class PreviewToolReducer {
class PreviewToolReducer: ReducerType {
typealias Pair = StateActionPair<UuidState<MainWindow.State>, PreviewTool.Action>
typealias StateType = MainWindow.State
typealias ActionType = UuidAction<PreviewTool.Action>
init(baseServerUrl: URL) {
self.baseServerUrl = baseServerUrl
}
func reduce(_ pair: Pair) -> Pair {
var state = pair.state.payload
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var state = tuple.state
switch pair.action {
switch tuple.action.payload {
case let .setAutomaticReverseSearch(to:value):
state.previewTool.isReverseSearchAutomatically = value
@ -28,11 +29,11 @@ class PreviewToolReducer {
state.previewTool.isRefreshOnWrite = value
default:
return pair
return tuple
}
return StateActionPair(state: UuidState(uuid: state.uuid, state: state), action: pair.action)
return (state, tuple.action, true)
}
private let baseServerUrl: URL

215
VimR/VimR/RxRedux.swift Normal file
View File

@ -0,0 +1,215 @@
/**
* Tae Won Ha - http://taewon.de - @hataewon
* See LICENSE
*/
import Foundation
import RxSwift
protocol ReduxContextType {
/**
Type that holds the global app state
- Important:
This type must be typealias'ed in `ReduxTypes` or in an extension thereof.
*/
associatedtype StateType
/**
"The greatest common divisor" for all actions used in the app: Assuming it is set to `ReduxTypes.ActionType` type,
the following must be true for any action
```
assert(someAction is ReduxTypes.ActionType) // which means
let actionWithMinimumType: ReduxTypes.ActionType = anyAction
```
Most probably this type will be set to `Any`.
- Important:
This type must be typealias'ed in `ReduxTypes` or in an extension thereof.
*/
associatedtype ActionType
typealias ReduceTuple = (state: StateType, action: ActionType, modified: Bool)
typealias ReduceFunction = (ReduceTuple) -> ReduceTuple
}
/**
`typealias` `StateType` and `ActionType` either within the class definition or in an extension.
*/
class ReduxTypes: ReduxContextType {
}
protocol ReducerType {
associatedtype StateType
associatedtype ActionType
typealias ReduceTuple = (state: StateType, action: ActionType, modified: Bool)
typealias ActionTypeErasedReduceTuple = (state: StateType, action: ReduxTypes.ActionType, modified: Bool)
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple
}
extension ReducerType {
func reduce(_ tuple: ActionTypeErasedReduceTuple) -> ActionTypeErasedReduceTuple {
guard let typedTuple = tuple as? ReduceTuple else {
return tuple
}
let typedResult = self.typedReduce(typedTuple)
return (state: typedResult.state, action: typedResult.action, modified: typedResult.modified)
}
}
protocol MiddlewareType {
associatedtype StateType
associatedtype ActionType
typealias ReduceTuple = (state: StateType, action: ActionType, modified: Bool)
typealias ActionTypeErasedReduceTuple = (state: StateType, action: ReduxTypes.ActionType, modified: Bool)
typealias TypedActionReduceFunction = (ReduceTuple) -> ActionTypeErasedReduceTuple
typealias ActionTypeErasedReduceFunction = (ActionTypeErasedReduceTuple) -> ActionTypeErasedReduceTuple
func typedApply(_ reduce: @escaping TypedActionReduceFunction) -> TypedActionReduceFunction
}
extension MiddlewareType {
func apply(_ reduce: @escaping ActionTypeErasedReduceFunction) -> ActionTypeErasedReduceFunction {
return { tuple in
guard let typedTuple = tuple as? ReduceTuple else {
return reduce(tuple)
}
let typedReduce: (ReduceTuple) -> ActionTypeErasedReduceTuple = { typedTuple in
// We know that we can cast the typed action to ReduxTypes.ActionType
return reduce((state: typedTuple.state, action: typedTuple.action, modified: typedTuple.modified))
}
return self.typedApply(typedReduce)(typedTuple)
}
}
}
protocol UiComponent {
associatedtype StateType
init(source: Observable<StateType>, emitter: ActionEmitter, state: StateType)
}
class ActionEmitter {
let observable: Observable<ReduxTypes.ActionType>
init() {
self.observable = self.subject.asObservable().observeOn(scheduler)
}
func typedEmit<T>() -> ((T) -> Void) {
return { (action: T) in
self.subject.onNext(action)
}
}
func terminate() {
self.subject.onCompleted()
}
deinit {
self.subject.onCompleted()
}
private let scheduler = SerialDispatchQueueScheduler(qos: .userInteractive)
private let subject = PublishSubject<ReduxTypes.ActionType>()
}
class ReduxContext {
let actionEmitter = ActionEmitter()
let stateSource: Observable<ReduxTypes.StateType>
convenience init(initialState: ReduxTypes.StateType,
reducers: [ReduxTypes.ReduceFunction],
middlewares: [(@escaping ReduxTypes.ReduceFunction) -> ReduxTypes.ReduceFunction] = []) {
self.init(initialState: initialState)
self.actionEmitter.observable
.map { (state: self.state, action: $0, modified: false) }
.reduce(by: reducers, middlewares: middlewares)
.filter { $0.modified }
.subscribe(onNext: { tuple in
self.state = tuple.state
self.stateSubject.onNext(tuple.state)
})
.disposed(by: self.disposeBag)
}
init(initialState: ReduxTypes.StateType) {
self.state = initialState
self.stateSource = self.stateSubject.asObservable().observeOn(self.stateScheduler)
}
func terminate() {
self.actionEmitter.terminate()
self.stateSubject.onCompleted()
}
var state: ReduxTypes.StateType
let stateSubject = PublishSubject<ReduxTypes.StateType>()
let stateScheduler = SerialDispatchQueueScheduler(qos: .userInteractive)
let disposeBag = DisposeBag()
}
extension Observable {
func completableSubject() -> CompletableSubject<Element> {
return CompletableSubject(source: self)
}
}
class CompletableSubject<T> {
func asObservable() -> Observable<T> {
return self.subject.asObservable()
}
init(source: Observable<T>) {
let subject = PublishSubject<T>()
self.subscription = source.subscribe(onNext: { element in subject.onNext(element) })
self.subject = subject
}
func onCompleted() {
self.subject.onCompleted()
self.subscription.dispose()
}
private let subject: PublishSubject<T>
private let subscription: Disposable
}
extension Observable {
func reduce(
by reducers: [(Element) -> Element],
middlewares: [(@escaping (Element) -> Element) -> (Element) -> Element]
) -> Observable<Element> {
let dispatch = { pair in
return reducers.reduce(pair) { result, reduceBody in
return reduceBody(result)
}
}
let next = middlewares.reversed().reduce(dispatch) { result, middleware in middleware(result) }
return self.map(next)
}
}

View File

@ -14,31 +14,6 @@ extension Observable {
.filter { $0 != nil }
.map { $0! }
}
func completableSubject() -> CompletableSubject<Element> {
return CompletableSubject(source: self)
}
}
class CompletableSubject<T> {
func asObservable() -> Observable<T> {
return self.subject.asObservable()
}
init(source: Observable<T>) {
let subject = PublishSubject<T>()
self.subscription = source.subscribe(subject)
self.subject = subject
}
func onCompleted() {
self.subject.onCompleted()
self.subscription.dispose()
}
private let subject: PublishSubject<T>
private let subscription: Disposable
}
extension PrimitiveSequenceType where TraitType == CompletableTrait, ElementType == Never {

View File

@ -5,11 +5,12 @@
import Foundation
class ToolsPrefReducer {
class ToolsPrefReducer: ReducerType {
typealias Pair = StateActionPair<AppState, ToolsPref.Action>
typealias StateType = AppState
typealias ActionType = ToolsPref.Action
func reduce(_ pair: Pair) -> Pair {
func typedReduce(_ pair: ReduceTuple) -> ReduceTuple {
var state = pair.state
switch pair.action {
@ -19,6 +20,6 @@ class ToolsPrefReducer {
}
return StateActionPair(state: state, action: pair.action)
return (state, pair.action, true)
}
}

View File

@ -6,49 +6,19 @@
import Foundation
import RxSwift
protocol UiComponent {
struct StateActionPair<S, A> {
associatedtype StateType
init(source: Observable<StateType>, emitter: ActionEmitter, state: StateType)
var state: S
var action: A
var modified: Bool
}
class ActionEmitter {
protocol UuidTagged {
let observable: Observable<Any>
init() {
self.observable = self.subject.asObservable().observeOn(scheduler)
}
func typedEmit<T>() -> ((T) -> Void) {
return { (action: T) in
self.subject.onNext(action)
}
}
deinit {
self.subject.onCompleted()
}
private let scheduler = SerialDispatchQueueScheduler(qos: .userInteractive)
private let subject = PublishSubject<Any>()
var uuid: String { get }
}
class StateActionPair<S, A> {
let modified: Bool
let state: S
let action: A
init(state: S, action: A, modified: Bool = true) {
self.modified = modified
self.state = state
self.action = action
}
}
class UuidAction<A>: CustomStringConvertible {
class UuidAction<A>: UuidTagged, CustomStringConvertible {
let uuid: String
let payload: A
@ -63,7 +33,7 @@ class UuidAction<A>: CustomStringConvertible {
}
}
class UuidState<S>: CustomStringConvertible {
class UuidState<S>: UuidTagged, CustomStringConvertible {
let uuid: String
let payload: S
@ -116,23 +86,6 @@ class Marked<T>: CustomStringConvertible {
}
}
extension Observable {
func reduce(by reduce: @escaping (Element) -> Element) -> Observable<Element> {
return self.map(reduce)
}
func apply(_ apply: @escaping (Element) -> Void) -> Observable<Element> {
return self.do(onNext: apply)
}
func filterMapPair<S, A>() -> Observable<S> where Element == StateActionPair<S, A> {
return self
.filter { $0.modified }
.map { $0.state }
}
}
class UiComponentTemplate: UiComponent {
typealias StateType = State

View File

@ -5,92 +5,100 @@
import Foundation
class UiRootReducer {
class UiRootReducer: ReducerType {
typealias UiRootPair = StateActionPair<AppState, UiRoot.Action>
typealias MainWindowPair = StateActionPair<AppState, UuidAction<MainWindow.Action>>
typealias StateType = AppState
typealias ActionType = UiRoot.Action
func reduceUiRoot(_ pair: UiRootPair) -> UiRootPair {
var appState = pair.state
let mainWindow = MainWindowReducer()
switch pair.action {
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var appState = tuple.state
switch tuple.action {
case .quit:
appState.quit = true
}
return StateActionPair(state: appState, action: pair.action)
return (appState, tuple.action, true)
}
func reduceMainWindow(_ pair: MainWindowPair) -> MainWindowPair {
var appState = pair.state
let uuid = pair.action.uuid
class MainWindowReducer: ReducerType {
switch pair.action.payload {
typealias StateType = AppState
typealias ActionType = UuidAction<MainWindow.Action>
case let .becomeKey(isFullScreen):
appState.currentMainWindowUuid = uuid
appState.mainWindowTemplate = self.mainWindowTemplate(
from: appState.mainWindowTemplate,
new: appState.mainWindows[uuid] ?? appState.mainWindowTemplate,
isFullScreen: isFullScreen
)
func typedReduce(_ tuple: ReduceTuple) -> ReduceTuple {
var appState = tuple.state
let uuid = tuple.action.uuid
switch tuple.action.payload {
case let .becomeKey(isFullScreen):
appState.currentMainWindowUuid = uuid
appState.mainWindowTemplate = self.mainWindowTemplate(
from: appState.mainWindowTemplate,
new: appState.mainWindows[uuid] ?? appState.mainWindowTemplate,
isFullScreen: isFullScreen
)
case let .frameChanged(to:frame):
if uuid == appState.currentMainWindowUuid {
appState.mainWindowTemplate.frame = frame
}
case let .setToolsState(tools):
appState.mainWindowTemplate.orderedTools = tools.map { $0.0 }
case let .toggleAllTools(value):
appState.mainWindowTemplate.isAllToolsVisible = value
case let .toggleToolButtons(value):
appState.mainWindowTemplate.isToolButtonsVisible = value
case .close:
if appState.currentMainWindowUuid == uuid, let mainWindowToClose = appState.mainWindows[uuid] {
appState.mainWindowTemplate = self.mainWindowTemplate(from: appState.mainWindowTemplate,
new: mainWindowToClose,
isFullScreen: false)
appState.currentMainWindowUuid = nil
}
appState.mainWindows.removeValue(forKey: uuid)
case let .setTheme(theme):
appState.mainWindowTemplate.appearance.theme = Marked(theme)
default:
return tuple
case let .frameChanged(to:frame):
if uuid == appState.currentMainWindowUuid {
appState.mainWindowTemplate.frame = frame
}
case let .setToolsState(tools):
appState.mainWindowTemplate.orderedTools = tools.map { $0.0 }
return (appState, tuple.action, true)
}
case let .toggleAllTools(value):
appState.mainWindowTemplate.isAllToolsVisible = value
private func mainWindowTemplate(from old: MainWindow.State,
new: MainWindow.State,
isFullScreen: Bool) -> MainWindow.State {
case let .toggleToolButtons(value):
appState.mainWindowTemplate.isToolButtonsVisible = value
var result = old
case .close:
if appState.currentMainWindowUuid == uuid, let mainWindowToClose = appState.mainWindows[uuid] {
appState.mainWindowTemplate = self.mainWindowTemplate(from: appState.mainWindowTemplate,
new: mainWindowToClose,
isFullScreen: false)
appState.currentMainWindowUuid = nil
if !isFullScreen {
result.frame = new.frame
}
appState.mainWindows.removeValue(forKey: uuid)
case let .setTheme(theme):
appState.mainWindowTemplate.appearance.theme = Marked(theme)
default:
return pair
result.isAllToolsVisible = new.isAllToolsVisible
result.isToolButtonsVisible = new.isToolButtonsVisible
result.tools = new.tools
result.orderedTools = new.orderedTools
result.previewTool = new.previewTool
result.fileBrowserShowHidden = new.fileBrowserShowHidden
result.htmlPreview = .default
return result
}
return StateActionPair(state: appState, action: pair.action)
}
private func mainWindowTemplate(from old: MainWindow.State,
new: MainWindow.State,
isFullScreen: Bool) -> MainWindow.State {
var result = old
if !isFullScreen {
result.frame = new.frame
}
result.isAllToolsVisible = new.isAllToolsVisible
result.isToolButtonsVisible = new.isToolButtonsVisible
result.tools = new.tools
result.orderedTools = new.orderedTools
result.previewTool = new.previewTool
result.fileBrowserShowHidden = new.fileBrowserShowHidden
result.htmlPreview = .default
return result
}
}