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.
This commit is contained in:
Simon Michael 2016-06-19 09:00:04 -07:00
parent c4b3a4f996
commit 4923efefb9
8 changed files with 99 additions and 6 deletions

View File

@ -14,6 +14,7 @@ import Brick
import Brick.Widgets.List import Brick.Widgets.List
import Brick.Widgets.Edit import Brick.Widgets.Edit
import Brick.Widgets.Border (borderAttr) import Brick.Widgets.Border (borderAttr)
import Control.Monad
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
import Data.List import Data.List
import Data.Maybe import Data.Maybe
@ -32,6 +33,7 @@ import Hledger.UI.UIOptions
import Hledger.UI.UITypes import Hledger.UI.UITypes
import Hledger.UI.UIState import Hledger.UI.UIState
import Hledger.UI.UIUtils import Hledger.UI.UIUtils
import Hledger.UI.Editor
import Hledger.UI.RegisterScreen import Hledger.UI.RegisterScreen
import Hledger.UI.ErrorScreen import Hledger.UI.ErrorScreen
@ -266,6 +268,7 @@ asHandle ui0@UIState{
EvKey (KChar c) [] | c `elem` ['h','?'] -> continue $ setMode Help ui EvKey (KChar c) [] | c `elem` ['h','?'] -> continue $ setMode Help ui
EvKey (KChar 'g') [] -> liftIO (uiReloadJournalIfChanged copts d j ui) >>= continue 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 '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 '0') [] -> continue $ regenerateScreens j d $ setDepth (Just 0) ui
EvKey (KChar '1') [] -> continue $ regenerateScreens j d $ setDepth (Just 1) ui EvKey (KChar '1') [] -> continue $ regenerateScreens j d $ setDepth (Just 1) ui
EvKey (KChar '2') [] -> continue $ regenerateScreens j d $ setDepth (Just 2) ui EvKey (KChar '2') [] -> continue $ regenerateScreens j d $ setDepth (Just 2) ui

View File

@ -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

View File

@ -8,6 +8,7 @@ module Hledger.UI.ErrorScreen
) )
where where
import Control.Monad
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
import Data.Monoid import Data.Monoid
import Data.Time.Calendar (Day) import Data.Time.Calendar (Day)
@ -19,6 +20,7 @@ import Hledger.UI.UIOptions
import Hledger.UI.UITypes import Hledger.UI.UITypes
import Hledger.UI.UIState import Hledger.UI.UIState
import Hledger.UI.UIUtils import Hledger.UI.UIUtils
import Hledger.UI.Editor
errorScreen :: Screen errorScreen :: Screen
errorScreen = ErrorScreen{ 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 :: UIState -> Event -> EventM (Next UIState)
esHandle ui@UIState{ esHandle ui@UIState{
aScreen=s@ErrorScreen{} aScreen=ErrorScreen{}
,aopts=UIOpts{cliopts_=copts} ,aopts=UIOpts{cliopts_=copts}
,ajournal=j ,ajournal=j
,aMode=mode ,aMode=mode
@ -76,11 +78,12 @@ esHandle ui@UIState{
EvKey (KChar 'q') [] -> halt ui EvKey (KChar 'q') [] -> halt ui
EvKey KEsc [] -> continue $ resetScreens d ui EvKey KEsc [] -> continue $ resetScreens d ui
EvKey (KChar c) [] | c `elem` ['h','?'] -> continue $ setMode Help ui EvKey (KChar c) [] | c `elem` ['h','?'] -> continue $ setMode Help ui
EvKey (KChar 'g') [] -> do EvKey (KChar 'E') [] -> suspendAndResume $ void (runEditor endPos j) >> uiReloadJournalIfChanged copts d j (popScreen ui)
(ej, _) <- liftIO $ journalReloadIfChanged copts d j EvKey (KChar 'g') [] -> liftIO (uiReloadJournalIfChanged copts d j (popScreen ui)) >>= continue
case ej of -- (ej, _) <- liftIO $ journalReloadIfChanged copts d j
Left err -> continue ui{aScreen=s{esError=err}} -- show latest parse error -- case ej of
Right j' -> continue $ regenerateScreens j' d $ popScreen ui -- return to previous screen, and reload it -- 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 _ -> continue ui
esHandle _ _ = error "event handler called with wrong screen type, should not happen" esHandle _ _ = error "event handler called with wrong screen type, should not happen"

View File

@ -9,6 +9,7 @@ module Hledger.UI.RegisterScreen
where where
import Lens.Micro.Platform ((^.)) import Lens.Micro.Platform ((^.))
import Control.Monad
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
import Data.List import Data.List
import Data.List.Split (splitOn) import Data.List.Split (splitOn)
@ -32,6 +33,7 @@ import Hledger.UI.UIOptions
import Hledger.UI.UITypes import Hledger.UI.UITypes
import Hledger.UI.UIState import Hledger.UI.UIState
import Hledger.UI.UIUtils import Hledger.UI.UIUtils
import Hledger.UI.Editor
import Hledger.UI.TransactionScreen import Hledger.UI.TransactionScreen
import Hledger.UI.ErrorScreen import Hledger.UI.ErrorScreen
@ -248,6 +250,7 @@ rsHandle ui@UIState{
EvKey (KChar c) [] | c `elem` ['h','?'] -> continue $ setMode Help ui EvKey (KChar c) [] | c `elem` ['h','?'] -> continue $ setMode Help ui
EvKey (KChar 'g') [] -> liftIO (uiReloadJournalIfChanged copts d j ui) >>= continue 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 '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 'F') [] -> scrollTop >> (continue $ regenerateScreens j d $ toggleFlat ui)
EvKey (KChar 'Z') [] -> scrollTop >> (continue $ regenerateScreens j d $ toggleEmpty ui) EvKey (KChar 'Z') [] -> scrollTop >> (continue $ regenerateScreens j d $ toggleEmpty ui)
EvKey (KChar 'C') [] -> scrollTop >> (continue $ regenerateScreens j d $ toggleCleared ui) EvKey (KChar 'C') [] -> scrollTop >> (continue $ regenerateScreens j d $ toggleCleared ui)

View File

@ -8,6 +8,7 @@ module Hledger.UI.TransactionScreen
) )
where where
import Control.Monad
import Control.Monad.IO.Class (liftIO) import Control.Monad.IO.Class (liftIO)
import Data.List import Data.List
import Data.Monoid import Data.Monoid
@ -25,6 +26,7 @@ import Hledger.UI.UIOptions
import Hledger.UI.UITypes import Hledger.UI.UITypes
import Hledger.UI.UIState import Hledger.UI.UIState
import Hledger.UI.UIUtils import Hledger.UI.UIUtils
import Hledger.UI.Editor
import Hledger.UI.ErrorScreen import Hledger.UI.ErrorScreen
transactionScreen :: Screen transactionScreen :: Screen
@ -122,6 +124,7 @@ tsHandle ui@UIState{aScreen=s@TransactionScreen{tsTransaction=(i,t)
EvKey (KChar 'q') [] -> halt ui EvKey (KChar 'q') [] -> halt ui
EvKey KEsc [] -> continue $ resetScreens d ui EvKey KEsc [] -> continue $ resetScreens d ui
EvKey (KChar c) [] | c `elem` ['h','?'] -> continue $ setMode Help 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 EvKey (KChar 'g') [] -> do
d <- liftIO getCurrentDay d <- liftIO getCurrentDay
(ej, _) <- liftIO $ journalReloadIfChanged copts d j (ej, _) <- liftIO $ journalReloadIfChanged copts d j

View File

@ -19,6 +19,8 @@ import Hledger
import Hledger.UI.UITypes import Hledger.UI.UITypes
import Hledger.UI.UIState import Hledger.UI.UIState
-- ui
-- | Draw the help dialog, called when help mode is active. -- | Draw the help dialog, called when help mode is active.
helpDialog :: Widget helpDialog :: Widget
helpDialog = helpDialog =
@ -33,6 +35,7 @@ helpDialog =
str "MISC" str "MISC"
,renderKey ("h", "toggle help") ,renderKey ("h", "toggle help")
,renderKey ("a", "add transaction") ,renderKey ("a", "add transaction")
,renderKey ("E", "open editor")
,renderKey ("g", "reload data") ,renderKey ("g", "reload data")
,renderKey ("q", "quit") ,renderKey ("q", "quit")
,str " " ,str " "

View File

@ -70,6 +70,7 @@ executable hledger-ui
, HUnit , HUnit
, microlens >= 0.4 && < 0.5 , microlens >= 0.4 && < 0.5
, microlens-platform >= 0.2.3.1 && < 0.4 , microlens-platform >= 0.2.3.1 && < 0.4
, process >= 1.2
, safe >= 0.2 , safe >= 0.2
, split >= 0.1 && < 0.3 , split >= 0.1 && < 0.3
, text >= 1.2 && < 1.3 , text >= 1.2 && < 1.3
@ -92,6 +93,7 @@ executable hledger-ui
other-modules: other-modules:
Hledger.UI Hledger.UI
Hledger.UI.AccountsScreen Hledger.UI.AccountsScreen
Hledger.UI.Editor
Hledger.UI.ErrorScreen Hledger.UI.ErrorScreen
Hledger.UI.Main Hledger.UI.Main
Hledger.UI.RegisterScreen Hledger.UI.RegisterScreen

View File

@ -75,6 +75,7 @@ executables:
- HUnit - HUnit
- microlens >= 0.4 && < 0.5 - microlens >= 0.4 && < 0.5
- microlens-platform >= 0.2.3.1 && < 0.4 - microlens-platform >= 0.2.3.1 && < 0.4
- process >= 1.2
- safe >= 0.2 - safe >= 0.2
- split >= 0.1 && < 0.3 - split >= 0.1 && < 0.3
- text >= 1.2 && < 1.3 - text >= 1.2 && < 1.3