From 4923efefb912e019feffe1ab0866c3d50cf7b093 Mon Sep 17 00:00:00 2001 From: Simon Michael Date: Sun, 19 Jun 2016 09:00:04 -0700 Subject: [PATCH] ui: basic editor integration The E key (on all screens) edits the main journal file using $HLEDGER_UI_EDITOR or $EDITOR or "emacs -nw", jumping to the end if it's Emacs. --- hledger-ui/Hledger/UI/AccountsScreen.hs | 3 + hledger-ui/Hledger/UI/Editor.hs | 75 ++++++++++++++++++++++ hledger-ui/Hledger/UI/ErrorScreen.hs | 15 +++-- hledger-ui/Hledger/UI/RegisterScreen.hs | 3 + hledger-ui/Hledger/UI/TransactionScreen.hs | 3 + hledger-ui/Hledger/UI/UIUtils.hs | 3 + hledger-ui/hledger-ui.cabal | 2 + hledger-ui/package.yaml | 1 + 8 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 hledger-ui/Hledger/UI/Editor.hs diff --git a/hledger-ui/Hledger/UI/AccountsScreen.hs b/hledger-ui/Hledger/UI/AccountsScreen.hs index 8550ded25..1cfe009eb 100644 --- a/hledger-ui/Hledger/UI/AccountsScreen.hs +++ b/hledger-ui/Hledger/UI/AccountsScreen.hs @@ -14,6 +14,7 @@ import Brick import Brick.Widgets.List import Brick.Widgets.Edit import Brick.Widgets.Border (borderAttr) +import Control.Monad import Control.Monad.IO.Class (liftIO) import Data.List import Data.Maybe @@ -32,6 +33,7 @@ import Hledger.UI.UIOptions import Hledger.UI.UITypes import Hledger.UI.UIState import Hledger.UI.UIUtils +import Hledger.UI.Editor import Hledger.UI.RegisterScreen import Hledger.UI.ErrorScreen @@ -266,6 +268,7 @@ asHandle ui0@UIState{ EvKey (KChar c) [] | c `elem` ['h','?'] -> continue $ setMode Help ui EvKey (KChar 'g') [] -> liftIO (uiReloadJournalIfChanged copts d j ui) >>= continue EvKey (KChar 'a') [] -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add copts j >> uiReloadJournalIfChanged copts d j ui + EvKey (KChar 'E') [] -> suspendAndResume $ void (runEditor endPos j) >> uiReloadJournalIfChanged copts d j ui EvKey (KChar '0') [] -> continue $ regenerateScreens j d $ setDepth (Just 0) ui EvKey (KChar '1') [] -> continue $ regenerateScreens j d $ setDepth (Just 1) ui EvKey (KChar '2') [] -> continue $ regenerateScreens j d $ setDepth (Just 2) ui diff --git a/hledger-ui/Hledger/UI/Editor.hs b/hledger-ui/Hledger/UI/Editor.hs new file mode 100644 index 000000000..579312b9f --- /dev/null +++ b/hledger-ui/Hledger/UI/Editor.hs @@ -0,0 +1,75 @@ +{- | Editor integration. -} + +-- {-# LANGUAGE OverloadedStrings #-} + +module Hledger.UI.Editor +where + +import Control.Applicative ((<|>)) +import Data.List +import Safe +import System.Environment +import System.Exit +import System.FilePath +import System.Process + +import Hledger + +-- | A shell command line for invoking an editor, containing one placeholder ("FILE") +-- which will be replaced with a quoted file path. This exists because some desirable +-- editor commands do not fit the simple "$EDITOR FILE" pattern. +type EditorCommandTemplate = String + +-- | Editors we know how to create more specific command lines for. +data EditorType = Emacs | Other + +-- | A position we can move to in a text editor: a line number +-- and optionally character number. 1 (or 0) means the first; a negative number +-- counts back from the end (so -1 means the last line, -2 the second last etc.) +type TextPosition = (Int, Maybe Int) + +endPos :: Maybe TextPosition +endPos = Just (1,Nothing) + +-- | Construct a shell command template for starting the user's preferred text editor, +-- optionally at a given position. +-- XXX The position parameter is currently ignored and assumed to be end-of-file. +-- +-- The basic editor command will be the value of environment variable $HLEDGER_UI_EDITOR, +-- or $EDITOR, or "emacs -nw". If a position is specified, and the command looks like one of +-- the editors we know (currently only emacs and emacsclient), it is modified so as to jump +-- to that position. +-- +-- Some examples: +-- $EDITOR=vi -> "vi FILE" +-- $EDITOR=emacs -> "emacs FILE -f end-of-buffer" +-- $EDITOR not set -> "emacs -nw FILE -f end-of-buffer" +-- +editorCommandTemplate :: Maybe TextPosition -> IO EditorCommandTemplate +editorCommandTemplate mpos = do + hledger_ui_editor_env <- lookupEnv "HLEDGER_UI_EDITOR" + editor_env <- lookupEnv "EDITOR" + let Just exe = hledger_ui_editor_env <|> editor_env <|> Just "emacs -nw" + return $ + case (identifyEditor exe, mpos) of + (Emacs,_) -> exe ++ " FILE -f end-of-buffer" + _ -> exe ++ " FILE" + +-- Identify the editor type, if we know it, from the value of $HLEDGER_EDITOR_UI or $EDITOR. +identifyEditor :: String -> EditorType +identifyEditor cmd + | "emacs" `isPrefixOf` exe = Emacs + | otherwise = Other + where + exe = lowercase $ takeFileName $ headDef "" $ words' cmd + +fillEditorCommandTemplate :: FilePath -> EditorCommandTemplate -> String +fillEditorCommandTemplate f t = regexReplace "FILE" (singleQuoteIfNeeded f) t + +-- | Try running $EDITOR, or a default edit command, on the main journal file, +-- blocking until it exits, and returning the exit code; or raise an error. +runEditor :: Maybe TextPosition -> Journal -> IO ExitCode +runEditor mpos j = do + fillEditorCommandTemplate (journalFilePath j) <$> editorCommandTemplate mpos + >>= runCommand + >>= waitForProcess diff --git a/hledger-ui/Hledger/UI/ErrorScreen.hs b/hledger-ui/Hledger/UI/ErrorScreen.hs index 88686f544..df6dd1db9 100644 --- a/hledger-ui/Hledger/UI/ErrorScreen.hs +++ b/hledger-ui/Hledger/UI/ErrorScreen.hs @@ -8,6 +8,7 @@ module Hledger.UI.ErrorScreen ) where +import Control.Monad import Control.Monad.IO.Class (liftIO) import Data.Monoid import Data.Time.Calendar (Day) @@ -19,6 +20,7 @@ import Hledger.UI.UIOptions import Hledger.UI.UITypes import Hledger.UI.UIState import Hledger.UI.UIUtils +import Hledger.UI.Editor errorScreen :: Screen errorScreen = ErrorScreen{ @@ -59,7 +61,7 @@ esDraw _ = error "draw function called with wrong screen type, should not happen esHandle :: UIState -> Event -> EventM (Next UIState) esHandle ui@UIState{ - aScreen=s@ErrorScreen{} + aScreen=ErrorScreen{} ,aopts=UIOpts{cliopts_=copts} ,ajournal=j ,aMode=mode @@ -76,11 +78,12 @@ esHandle ui@UIState{ EvKey (KChar 'q') [] -> halt ui EvKey KEsc [] -> continue $ resetScreens d ui EvKey (KChar c) [] | c `elem` ['h','?'] -> continue $ setMode Help ui - EvKey (KChar 'g') [] -> do - (ej, _) <- liftIO $ journalReloadIfChanged copts d j - case ej of - Left err -> continue ui{aScreen=s{esError=err}} -- show latest parse error - Right j' -> continue $ regenerateScreens j' d $ popScreen ui -- return to previous screen, and reload it + EvKey (KChar 'E') [] -> suspendAndResume $ void (runEditor endPos j) >> uiReloadJournalIfChanged copts d j (popScreen ui) + EvKey (KChar 'g') [] -> liftIO (uiReloadJournalIfChanged copts d j (popScreen ui)) >>= continue +-- (ej, _) <- liftIO $ journalReloadIfChanged copts d j +-- case ej of +-- Left err -> continue ui{aScreen=s{esError=err}} -- show latest parse error +-- Right j' -> continue $ regenerateScreens j' d $ popScreen ui -- return to previous screen, and reload it _ -> continue ui esHandle _ _ = error "event handler called with wrong screen type, should not happen" diff --git a/hledger-ui/Hledger/UI/RegisterScreen.hs b/hledger-ui/Hledger/UI/RegisterScreen.hs index b8cae0951..fcafb68d8 100644 --- a/hledger-ui/Hledger/UI/RegisterScreen.hs +++ b/hledger-ui/Hledger/UI/RegisterScreen.hs @@ -9,6 +9,7 @@ module Hledger.UI.RegisterScreen where import Lens.Micro.Platform ((^.)) +import Control.Monad import Control.Monad.IO.Class (liftIO) import Data.List import Data.List.Split (splitOn) @@ -32,6 +33,7 @@ import Hledger.UI.UIOptions import Hledger.UI.UITypes import Hledger.UI.UIState import Hledger.UI.UIUtils +import Hledger.UI.Editor import Hledger.UI.TransactionScreen import Hledger.UI.ErrorScreen @@ -248,6 +250,7 @@ rsHandle ui@UIState{ EvKey (KChar c) [] | c `elem` ['h','?'] -> continue $ setMode Help ui EvKey (KChar 'g') [] -> liftIO (uiReloadJournalIfChanged copts d j ui) >>= continue EvKey (KChar 'a') [] -> suspendAndResume $ clearScreen >> setCursorPosition 0 0 >> add copts j >> uiReloadJournalIfChanged copts d j ui + EvKey (KChar 'E') [] -> suspendAndResume $ void (runEditor endPos j) >> uiReloadJournalIfChanged copts d j ui EvKey (KChar 'F') [] -> scrollTop >> (continue $ regenerateScreens j d $ toggleFlat ui) EvKey (KChar 'Z') [] -> scrollTop >> (continue $ regenerateScreens j d $ toggleEmpty ui) EvKey (KChar 'C') [] -> scrollTop >> (continue $ regenerateScreens j d $ toggleCleared ui) diff --git a/hledger-ui/Hledger/UI/TransactionScreen.hs b/hledger-ui/Hledger/UI/TransactionScreen.hs index cf0f88af6..a7b5931c2 100644 --- a/hledger-ui/Hledger/UI/TransactionScreen.hs +++ b/hledger-ui/Hledger/UI/TransactionScreen.hs @@ -8,6 +8,7 @@ module Hledger.UI.TransactionScreen ) where +import Control.Monad import Control.Monad.IO.Class (liftIO) import Data.List import Data.Monoid @@ -25,6 +26,7 @@ import Hledger.UI.UIOptions import Hledger.UI.UITypes import Hledger.UI.UIState import Hledger.UI.UIUtils +import Hledger.UI.Editor import Hledger.UI.ErrorScreen transactionScreen :: Screen @@ -122,6 +124,7 @@ tsHandle ui@UIState{aScreen=s@TransactionScreen{tsTransaction=(i,t) EvKey (KChar 'q') [] -> halt ui EvKey KEsc [] -> continue $ resetScreens d ui EvKey (KChar c) [] | c `elem` ['h','?'] -> continue $ setMode Help ui + EvKey (KChar 'E') [] -> suspendAndResume $ void (runEditor endPos j) >> uiReloadJournalIfChanged copts d j ui EvKey (KChar 'g') [] -> do d <- liftIO getCurrentDay (ej, _) <- liftIO $ journalReloadIfChanged copts d j diff --git a/hledger-ui/Hledger/UI/UIUtils.hs b/hledger-ui/Hledger/UI/UIUtils.hs index df3c1dcdd..ca1a5dcd1 100644 --- a/hledger-ui/Hledger/UI/UIUtils.hs +++ b/hledger-ui/Hledger/UI/UIUtils.hs @@ -19,6 +19,8 @@ import Hledger import Hledger.UI.UITypes import Hledger.UI.UIState +-- ui + -- | Draw the help dialog, called when help mode is active. helpDialog :: Widget helpDialog = @@ -33,6 +35,7 @@ helpDialog = str "MISC" ,renderKey ("h", "toggle help") ,renderKey ("a", "add transaction") + ,renderKey ("E", "open editor") ,renderKey ("g", "reload data") ,renderKey ("q", "quit") ,str " " diff --git a/hledger-ui/hledger-ui.cabal b/hledger-ui/hledger-ui.cabal index a153393cf..d9ed0d711 100644 --- a/hledger-ui/hledger-ui.cabal +++ b/hledger-ui/hledger-ui.cabal @@ -70,6 +70,7 @@ executable hledger-ui , HUnit , microlens >= 0.4 && < 0.5 , microlens-platform >= 0.2.3.1 && < 0.4 + , process >= 1.2 , safe >= 0.2 , split >= 0.1 && < 0.3 , text >= 1.2 && < 1.3 @@ -92,6 +93,7 @@ executable hledger-ui other-modules: Hledger.UI Hledger.UI.AccountsScreen + Hledger.UI.Editor Hledger.UI.ErrorScreen Hledger.UI.Main Hledger.UI.RegisterScreen diff --git a/hledger-ui/package.yaml b/hledger-ui/package.yaml index b5aa5050e..15576b09e 100644 --- a/hledger-ui/package.yaml +++ b/hledger-ui/package.yaml @@ -75,6 +75,7 @@ executables: - HUnit - microlens >= 0.4 && < 0.5 - microlens-platform >= 0.2.3.1 && < 0.4 + - process >= 1.2 - safe >= 0.2 - split >= 0.1 && < 0.3 - text >= 1.2 && < 1.3