packages feed

ansido-1.0.0: src/Ansido/App/State.hs

-- | Application state and the actions that can change it.
module Ansido.App.State
    ( Screen (..)
    , Filter (..)
    , Sort (..)
    , RowIndex
    , rowIndexValue
    , mkRowIndex
    , UiState (..)
    , AppState (..)
    , Direction (..)
    , Action (..)
    , initialAppState
    , initialUiState
    , withDataFile
    , currentCategory
    , visibleTasks
    , selectedTaskId
    , selectedTask
    , filterTasks
    , filterLabel
    , nextFilter
    , sortLabel
    , nextSort
    , withNotice
    ) where

--------------------------------------------------------------------------------
import Data.Char (toLower)
import Data.List (sortOn)
import Data.Maybe (listToMaybe)
import Numeric.Natural (Natural)

import Ansido.Domain
import Ansido.Ui.Components (Notice)

-- | Which screen is currently visible.
data Screen
    = BoardScreen
    | DetailsScreen TaskId
    | EditTaskScreen TaskId
    | HelpScreen
    | CategoryScreen
    | AddTaskScreen
    | AddCategoryScreen
    | EditCategoryScreen CategoryId
    deriving (Eq, Show)

-- | Zero based position inside the task table.
newtype RowIndex = RowIndex {rowIndexValue :: Natural}
    deriving (Eq, Ord, Show)

-- | Builds a row index, clamping negative values to zero.
mkRowIndex :: Int -> RowIndex
mkRowIndex raw = RowIndex (fromIntegral (max 0 raw))

-- | Which statuses the table shows. 'ShowAll' disables the filter.
data Filter = ShowAll | OnlyStatus Status
    deriving (Eq, Show)

-- | Order of the task table. 'SortOriginal' keeps the insertion order.
data Sort = SortOriginal | SortByStatus | SortByTitle
    deriving (Eq, Show)

-- | Everything the interface needs besides the workspace.
data UiState = UiState
    { uiScreen :: Screen
    , uiCategory :: Maybe CategoryId
    , uiFilter :: Filter
    , uiSort :: Sort
    , uiRow :: RowIndex
    , uiNotice :: Maybe Notice
    }
    deriving (Eq, Show)

-- | The whole application state.
data AppState = AppState
    { appWorkspace :: Workspace
    , appUi :: UiState
    , appDataFile :: FilePath
    }
    deriving (Eq, Show)

-- | Direction of a discrete movement.
data Direction = Previous | Next
    deriving (Eq, Show)

-- | Every state change requested by the interface, handled by a pure update.
data Action
    = OpenBoard
    | OpenHelp
    | OpenCategories
    | OpenDetails
    | OpenEditTask
    | OpenAddTask
    | OpenAddCategory
    | OpenEditCategory
    | SelectCategory CategoryId
    | MoveRow Direction
    | MoveCategory Direction
    | CycleFilter
    | CycleSort
    | CycleSelected
    | DeleteSelected
    | DeleteSelectedCategory
    | SubmitTask String String
    | SubmitEdit TaskId String String
    | SubmitCategory String
    | SubmitCategoryEdit CategoryId String
    | SetNotice Notice
    | ClearNotice
    deriving (Eq, Show)

-- | Builds the initial state on the category screen.
initialAppState :: Workspace -> AppState
initialAppState workspace =
    AppState
        { appWorkspace = workspace
        , appUi = initialUiState workspace
        , appDataFile = ""
        }

-- | Records the data file path shown on the help screen.
withDataFile :: FilePath -> AppState -> AppState
withDataFile path app =
    app{appDataFile = path}

-- | Initial interface state, always starting on the category screen.
initialUiState :: Workspace -> UiState
initialUiState workspace =
    UiState
        { uiScreen = CategoryScreen
        , uiCategory = fmap categoryId (listToMaybe (workspaceCategories workspace))
        , uiFilter = ShowAll
        , uiSort = SortOriginal
        , uiRow = mkRowIndex 0
        , uiNotice = Nothing
        }

-- | The selected category, when one exists.
currentCategory :: AppState -> Maybe Category
currentCategory app =
    uiCategory (appUi app) >>= \identifier ->
        findCategory identifier (appWorkspace app)

-- | Tasks visible in the selected category, filtered and sorted.
visibleTasks :: AppState -> [Task]
visibleTasks app =
    case uiCategory (appUi app) of
        Nothing -> []
        Just identifier ->
            orderTasks
                (uiFilter (appUi app))
                (uiSort (appUi app))
                (categoryTasks identifier (appWorkspace app))

-- | Applies the filter and then the sort order.
orderTasks :: Filter -> Sort -> [Task] -> [Task]
orderTasks filterValue sortValue =
    sortTasks sortValue . filterTasks filterValue

-- | Sorts tasks, keeping equal entries in their original order.
sortTasks :: Sort -> [Task] -> [Task]
sortTasks SortOriginal = id
sortTasks SortByStatus = sortOn taskStatus
sortTasks SortByTitle = sortOn (map toLower . taskTitleText . taskTitle)

-- | Applies the state filter to a list of tasks.
filterTasks :: Filter -> [Task] -> [Task]
filterTasks ShowAll = id
filterTasks (OnlyStatus status) = tasksByStatus status

-- | Human readable label of a filter.
filterLabel :: Filter -> String
filterLabel ShowAll = "all"
filterLabel (OnlyStatus status) = statusLabel status

-- | Advances the filter through all, Todo, Doing and Done.
nextFilter :: Filter -> Filter
nextFilter ShowAll = OnlyStatus Todo
nextFilter (OnlyStatus Todo) = OnlyStatus Doing
nextFilter (OnlyStatus Doing) = OnlyStatus Done
nextFilter (OnlyStatus Done) = ShowAll

-- | Human readable label of a sort order.
sortLabel :: Sort -> String
sortLabel SortOriginal = "original"
sortLabel SortByStatus = "status"
sortLabel SortByTitle = "title"

-- | Advances the sort order through original, status and title.
nextSort :: Sort -> Sort
nextSort SortOriginal = SortByStatus
nextSort SortByStatus = SortByTitle
nextSort SortByTitle = SortOriginal

-- | Task under the cursor, or the task open on the details or edit screen.
selectedTaskId :: AppState -> Maybe TaskId
selectedTaskId app =
    case uiScreen (appUi app) of
        DetailsScreen identifier -> Just identifier
        EditTaskScreen identifier -> Just identifier
        _ ->
            taskId
                <$> indexMaybe
                    (fromIntegral (rowIndexValue (uiRow (appUi app))))
                    (visibleTasks app)

-- | Task under the cursor, when it still exists.
selectedTask :: AppState -> Maybe Task
selectedTask app =
    selectedTaskId app >>= \identifier ->
        findTask identifier (appWorkspace app)

-- | Replaces the current notice.
withNotice :: Notice -> AppState -> AppState
withNotice notice app =
    app{appUi = (appUi app){uiNotice = Just notice}}