
hi all, i'm looking for advice on how to architect a simple widget library for vty/hscurses, but these beginner-level questions should apply to haskell applications in general. input/requests on any aspect of my design would be greatly appreciated - in return perhaps you'll get something you'd actually want to use! i have studied in detail various haskell curses/vty apps, including yi, hmp3, riot, and hscurses.widgets/contactmanager. my immediate goal is to produce a set of composable widgets and a flexible framework that - as much as possible - reduces the amount of code a user has to write. my eventual goal is to make a functional gui library, along the lines of fruit/wxfruit/fg. to this end i've also read their literature, but i still do not understand enough about arrows/yampa/afrp. i currently have a tree of widgets, some of which can receive focus. this allows them to alter the program's key handling, but in a certain order based on the hierarchy. e.g., in my program, at the top-level, hitting either 'q' or 'f12' quits the program. when we focus on a container, pressing 'tab' will cycle focus among the subwidgets. when we focus on a text box, the key bindings layer, so that 'q' inserts a character into the text box, and 'f12' still exits and 'tab' still cycles focus. here's a simplified synopsis of the types (omits details like layout): class Widget w where output :: Bool {- whether we have focus -} -> w -> Output data Output = Output Image (Maybe CursorPos) KeyHandler type Image = [[(Char,Attr)]] -- Attr is just the text decoration (colors) type CursorPos = (Int,Int) type KeyHandler = ??? i'm guessing the most flexible type for KeyHandler could be Key -> IO (), but is this really the only/best approach? (every time i fall back to IO, i feel i'm missing something/a puppy dies/etc.) currently, this is what i have: type KeyHandler = Key -> AppState -> AppState -- maps to a state updater data AppState = AppState { rootWidget :: Widget, actualAppState :: ..., ... } but this has a number of apparent issues, including: - no decoupling of UI and app - that is, the key handlers that the widgets return have complete knowledge of the application state. so for instance, this particular text box knows that its key handler should be: handler key state = let oldPos = cursorPos $ someTextBox $ rootWidget $ state in case key of Left -> state { rootWidget { someTextBox { cursorPos = oldPos - 1 } } } ... - on key press, can't save to a file or otherwise do anything in IO. - this isn't going to scale - as my AppState grows and grows, we're throwing away and reconstructing a lot of state. and the coding style it demands is pretty clumsy, as demonstrated above. but if we switch to IO (): - still doesn't help decouple the library from the app. the above example key handling code snippet would be the same (i.e. still very clumsy), except that we'd be reading/writing the state from/to an IORef or MVar. - certainly, not all actions need IO. in fact, my current application is just a viewer, and thus needs no IO at all. - requires a global IORef or MVar - i don't know how to address the performance problem without resorting to sprinkling IORefs or MVars everywhere in the state structure, thus strangling the app into IO everywhere other open questions: - how should the "top-most" code work? currently, my app's main has a tiny event loop that feeds keys through a Chan to a State-ful function ([Key] -> State AppState [Image]) and then to the final drawing function ([Image] -> IO ()). however, depending on the resolutions to the above issues, this may radically change. - how should i compose the various key handlers? this, again, will depend. related work: yi/hmp3 also have one large piece of state in a global mvar (allows multiple threads to update it/trigger a redraw, instead of only redrawing in response to key events), with no attempt to decouple the UI and app parts of that state. event handling is done by a lexer that matches regex key patterns to IO () - this doesn't couple key handling with the UI components, and is thus not composable. (i thought the idea of using a lexer as the state automata was good - there may be some way to make this more composable, too, if the regex ever fails.) riot's UI code operates mostly in StateT Riot IO (), where Riot is again a monolithic application state. again, no attempt at decoupling or composability is made. hscurses.widgets operates mostly in IO (), and 'activating' (focusing) on widgets hands over the event loop completely. i hope i explained my design problems clearly. i've used haskell a bunch for various small text-processing scripts, but decided to try to use it for a "real" application that has little to do with parsing or other purported strengths of the language. i believe other new haskellers may relate to some of these issues. thanks! yang