From eafc5de32eb10e2b1a86e9dbb5f90404a6452748 Mon Sep 17 00:00:00 2001 From: yosukeueda33 Date: Wed, 30 Jul 2025 20:13:36 +0900 Subject: [PATCH 01/10] Mod readme giving args to fix ubuntu GUI issue. --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index e70cdb5..bf37fc1 100644 --- a/README.md +++ b/README.md @@ -92,6 +92,17 @@ If you built from the souce, execute this command in the cloned dir. make run ``` +If you get this output, +```bash +Setup of the rendering thread failed: Unable to make GL context current +Falling back to rendering in the main thread. The content may not be updated while resizing the window. +``` +this may fix the problem. +```bash +__GLX_VENDOR_LIBRARY_NAME=mesa LIBGL_ALWAYS_SOFTWARE=1 ./chordMapper-x86_64.AppImage +__GLX_VENDOR_LIBRARY_NAME=mesa LIBGL_ALWAYS_SOFTWARE=1 make run +``` + This is the UI of the chordMapper. Sorry, it's still awful. Select input and output device that you want to use. And then, click `Play/Stop` button. From bd2832116d8aa8f8c4eb6a1187fbb4d4e40c2d8f Mon Sep 17 00:00:00 2001 From: yosukeueda33 Date: Thu, 31 Jul 2025 10:46:00 +0900 Subject: [PATCH 02/10] Add simple TUI. --- package.yaml | 2 +- src/Mapper.hs | 4 +- src/Tui.hs | 50 ++++++++++++++ src/Ui.hs | 179 -------------------------------------------------- stack.yaml | 7 +- 5 files changed, 54 insertions(+), 188 deletions(-) create mode 100644 src/Tui.hs delete mode 100644 src/Ui.hs diff --git a/package.yaml b/package.yaml index 16d8a19..6f1e0f5 100644 --- a/package.yaml +++ b/package.yaml @@ -32,13 +32,13 @@ dependencies: - stm - time - containers -- monomer - text - text-show - lens - dhall - filepath - directory +- brick ghc-options: - -Wall diff --git a/src/Mapper.hs b/src/Mapper.hs index 3f2e4c6..d876cd2 100644 --- a/src/Mapper.hs +++ b/src/Mapper.hs @@ -45,7 +45,7 @@ import Chord (Chord(..) , getVoicingBetweenOn, getEnvelopeDifference , degreeToChord7thOnePassingTension , addNoMin2ndTension) -import Ui +import Tui import Types import ArgParse import Rec @@ -135,7 +135,7 @@ mapperMain = do in bimap f f <$> getAllDevices :: IO ([(InputDeviceID, String)], [(OutputDeviceID, String)]) -- UI thread. let needOutSubDev = isMinilab3 config - (uiInput, uiUpdator) <- createUiThread devices needOutSubDev $ T.pack fontPath + (uiInput, uiUpdator) <- createTuiThread devices needOutSubDev initializeMidi -- Wait and execute UI input. let diff --git a/src/Tui.hs b/src/Tui.hs new file mode 100644 index 0000000..65eff0d --- /dev/null +++ b/src/Tui.hs @@ -0,0 +1,50 @@ +module Tui where + +import Control.Concurrent.MVar +import Control.Concurrent.STM +import Control.Concurrent (forkIO, threadDelay) +import Brick (Widget, simpleMain, (<+>), str, withBorderStyle, joinBorders) +import Brick.Widgets.Center (center) +import Brick.Widgets.Border (borderWithLabel, vBorder) +import Brick.Widgets.Border.Style (unicode) +import Euterpea.IO.MIDI.MidiIO + ( OutputDeviceID, InputDeviceID, unsafeInputID, unsafeOutputID ) + +data UiInput + = UiStart InputDeviceID OutputDeviceID + (Maybe OutputDeviceID) -- For like, lights on keyboard like MiniLab3 touch pads. + | UiStop + | UiExit + +type UiUpdator = (Maybe String, Maybe Int, Maybe Int) -> IO () +type DeviceLists = ([(InputDeviceID, String)], [(OutputDeviceID, String)]) + + +ui :: Widget () +ui = + joinBorders $ + withBorderStyle unicode $ + borderWithLabel (str "Hello!") + (center (str "Left") <+> vBorder <+> center (str "Right")) + +tuiMain :: MVar UiInput -> IO () +tuiMain mUiInput = do + _ <- simpleMain ui + putMVar mUiInput UiExit + +createTuiThread :: DeviceLists -> Bool -> IO (IO UiInput, UiUpdator) +createTuiThread devices needOutSubDev = do + mUiInput <- newEmptyMVar + tChordName' <- newTVarIO "" + tClockProgress' <- newTVarIO 0 + tChordSetProgress' <- newTVarIO 0 + _ <- forkIO $ tuiMain mUiInput + let + uiInput = takeMVar mUiInput + updateTVar tx = maybe (return ()) (atomically . writeTVar tx) + updator (mbChordName, mbClockProgress, mbChordSetProgress) + = updateTVar tChordName' mbChordName + >> updateTVar tClockProgress' mbClockProgress + >> updateTVar tChordSetProgress' mbChordSetProgress + + return (uiInput, updator) diff --git a/src/Ui.hs b/src/Ui.hs deleted file mode 100644 index 244b1cc..0000000 --- a/src/Ui.hs +++ /dev/null @@ -1,179 +0,0 @@ -{-# LANGUAGE OverloadedStrings #-} -{-# LANGUAGE TemplateHaskell #-} - -module Ui (UiInput(..), UiUpdator, createUiThread) where - -import Control.Lens -import qualified Data.Text as T -import Monomer - -import Control.Concurrent.MVar -import Control.Concurrent.STM -import Control.Concurrent (forkIO, threadDelay) - -import Euterpea.IO.MIDI.MidiIO - ( OutputDeviceID, InputDeviceID, unsafeInputID, unsafeOutputID ) - - -data PlayState = PlayStopped | PlayStopping | Playing deriving (Eq, Show) - -type DeviceLists = ([(InputDeviceID, String)], [(OutputDeviceID, String)]) - -data AppModel = AppModel { - _chordName :: String, - _playing :: PlayState, - _clockProgress :: Int, - _chordSetProgress :: Int, - _inDev :: (InputDeviceID, String), - _outDev :: (OutputDeviceID, String), - _outSubDev :: (OutputDeviceID, String) -} deriving (Eq, Show) - -data UiOutput = UiOutput { - tChordName :: TVar String, - tClockProgress :: TVar Int, - tChordSetProgress :: TVar Int -} - -type UiUpdator = (Maybe String, Maybe Int, Maybe Int) -> IO () - -data AppEvent - = AppInit - | AppStartStop - | AppStartDone - | AppStopDone - | AppSetBorder [Int] - | AppDispose - | AppDisposeDone - | AppExit - | AppExitDone - | AppUpdateChord String - | AppUpdateClockProgress Int - | AppUpdateChordSetProgress Int - deriving (Eq, Show) - -data UiInput - = UiStart InputDeviceID OutputDeviceID - (Maybe OutputDeviceID) -- For like, lights on keyboard like MiniLab3 touch pads. - | UiStop - | UiExit - -makeLenses 'AppModel - -when' :: Bool -> [a] -> [a] -when' True xs = xs -when' False _ = [] - -buildUI - :: DeviceLists - -> Bool - -> WidgetEnv AppModel AppEvent - -> AppModel - -> WidgetNode AppModel AppEvent -buildUI devices needOutSubDev _ model = widgetTree where - widgetTree = vstack ([ - label_ (T.pack $ model ^. chordName) [resizeFactor 1] `styleBasic` [textSize 24], - label_ (T.pack . genProgressString 1 $ model ^. chordSetProgress) [resizeFactor 1] `styleBasic` [textSize 24], - label_ (T.pack . genProgressString 6 $ model ^. clockProgress) [resizeFactor 1] `styleBasic` [textSize 24] - ] - ++ devLists - ++ [ - button "Play/Stop" AppStartStop - ]) `styleBasic` [padding 10] - devLists = [ - label_ "Select input device:" [] - , selectList inDev (fst devices) makeListLine - , label_ "Select output device:" [] - , selectList outDev (snd devices) makeListLine - ] - ++ when' needOutSubDev [ label_ "Select special output device:" [] - , selectList outSubDev (snd devices) makeListLine] - makeListLine (iD, name) = label . T.pack $ show iD ++ " " ++ name - -genProgressString :: Int -> Int -> String -genProgressString divNum x - | x >= 0 = concat . flip replicate "o" $ x `div` divNum - | otherwise = "" - -handleEvent - :: MVar UiInput -> UiOutput - -> Bool - -> WidgetEnv AppModel AppEvent - -> WidgetNode AppModel AppEvent - -> AppModel - -> AppEvent - -> [AppEventResponse AppModel AppEvent] -handleEvent mUiInput uiOutput needOutSubDev _ _ model evt = - let - startTask = let i = fst $ model ^. inDev - o = fst $ model ^. outDev - mbSubOut = if needOutSubDev then Just (fst $ model ^. outSubDev) else Nothing - cmd = UiStart i o mbSubOut - in putMVar mUiInput cmd >> return AppStartDone - stopTask = putMVar mUiInput UiStop >> return AppStopDone - disposeTask = return AppDisposeDone - exitTask = putMVar mUiInput UiExit >> return AppExitDone - in case evt of - AppInit -> [Producer $ outputUpdateProducer uiOutput] - AppStartStop -> case model ^. playing of - Playing -> [ Task stopTask - , Model $ model & playing .~ PlayStopping - , Model $ model & chordName .~ "" - ] - PlayStopped -> [Task startTask, Model $ model & playing .~ Playing] - PlayStopping -> [] - AppStopDone -> [Model $ model & playing .~ PlayStopped] - AppDispose -> [Task disposeTask] - AppExit -> [Task exitTask] - AppUpdateChord name -> [Model $ model & chordName .~ name] - AppUpdateClockProgress x -> [Model $ model & clockProgress .~ x] - AppUpdateChordSetProgress x -> [Model $ model & chordSetProgress .~ x] - _ -> [] - -outputUpdateProducer :: UiOutput -> (AppEvent -> IO ()) -> IO () -outputUpdateProducer uiOutput sendMsg = do - let f event elm = readTVarIO (elm uiOutput) >>= sendMsg . event - f AppUpdateChord tChordName - f AppUpdateClockProgress tClockProgress - f AppUpdateChordSetProgress tChordSetProgress - threadDelay 10000 - outputUpdateProducer uiOutput sendMsg - -uiMain :: MVar UiInput -> UiOutput -> DeviceLists -> Bool -> T.Text -> IO () -uiMain mUiInput uiOutput devices needOutSubDev fontPath = do - startApp model h b config - where - h = handleEvent mUiInput uiOutput needOutSubDev :: AppEventHandler AppModel AppEvent - b = buildUI devices needOutSubDev :: AppUIBuilder AppModel AppEvent - config = [ - appWindowTitle "KANNASHI chordMapper", - appTheme darkTheme, - appFontDef "Regular" fontPath, - appInitEvent AppInit, - appDisposeEvent AppDispose, - appExitEvent AppExit - ] - model = AppModel "" PlayStopped 0 0 - (unsafeInputID 0, "") (unsafeOutputID 0, "") - (unsafeOutputID 0, "") :: AppModel - - -createUiThread :: DeviceLists -> Bool -> T.Text -> IO (IO UiInput, UiUpdator) -createUiThread devices needOutSubDev fontPath = do - mUiInput <- newEmptyMVar - tChordName' <- newTVarIO "" - tClockProgress' <- newTVarIO 0 - tChordSetProgress' <- newTVarIO 0 - _ <- forkIO $ uiMain mUiInput - (UiOutput tChordName' tClockProgress' tChordSetProgress') - devices needOutSubDev fontPath - - let - uiInput = takeMVar mUiInput - updateTVar tx = maybe (return ()) (atomically . writeTVar tx) - updator (mbChordName, mbClockProgress, mbChordSetProgress) - = updateTVar tChordName' mbChordName - >> updateTVar tClockProgress' mbClockProgress - >> updateTVar tChordSetProgress' mbChordSetProgress - - return (uiInput, updator) \ No newline at end of file diff --git a/stack.yaml b/stack.yaml index 767e34c..58ee345 100644 --- a/stack.yaml +++ b/stack.yaml @@ -47,14 +47,9 @@ packages: # extra-deps: [] extra-deps: - PortMidi-0.2.0.0 -- monomer-1.6.0.1 -- nanovg-0.8.1.0 -- sdl2-2.5.5.1 # Override default flag values for project packages and extra-deps -flags: - sdl2: - recent-ish: false +# flags: [] # Extra package databases containing global packages # extra-package-dbs: [] From b740416d00eab69ddb2f2a671d1f810aa93b581a Mon Sep 17 00:00:00 2001 From: yosukeueda33 Date: Thu, 31 Jul 2025 14:45:57 +0900 Subject: [PATCH 03/10] Mod build error on windows. --- .gitmodules | 3 ++ package.yaml | 1 - src/Mapper.hs | 4 +-- stack-win.yaml | 80 ++++++++++++++++++++++++++++++++++++++++++++++++++ vty-windows | 1 + 5 files changed, 86 insertions(+), 3 deletions(-) create mode 100644 stack-win.yaml create mode 160000 vty-windows diff --git a/.gitmodules b/.gitmodules index 62b10e6..4d1e814 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "PortMidi"] path = PortMidi url = git@github.com:yosukeueda33/PortMidi-haskell.git +[submodule "vty-windows"] + path = vty-windows + url = https://github.com/chhackett/vty-windows.git diff --git a/package.yaml b/package.yaml index 6f1e0f5..caf591f 100644 --- a/package.yaml +++ b/package.yaml @@ -28,7 +28,6 @@ dependencies: - PortMidi - deepseq - HCodecs -- unix - stm - time - containers diff --git a/src/Mapper.hs b/src/Mapper.hs index d876cd2..08e726e 100644 --- a/src/Mapper.hs +++ b/src/Mapper.hs @@ -120,7 +120,7 @@ mapperMain = do checkFilePath "font" fontPath -- Load config. - config <- Dhall.input Dhall.auto $ T.pack cfgPath :: IO FullConfig + config <- Dhall.inputFile Dhall.auto cfgPath :: IO FullConfig print config -- Initialize buffers. @@ -631,4 +631,4 @@ sendMidiOut dev ms = outputMidi dev >> mapM_ (deliverMidiEvent dev . (0,) . snd) type Seconds = Double -- Wait specified seconds. wait :: Seconds -> IO () -wait s = threadDelay $ round $ s * 1000000 \ No newline at end of file +wait s = threadDelay $ round $ s * 1000000 diff --git a/stack-win.yaml b/stack-win.yaml new file mode 100644 index 0000000..3ac3088 --- /dev/null +++ b/stack-win.yaml @@ -0,0 +1,80 @@ +# This file was automatically generated by 'stack init' +# +# Some commonly used options have been documented as comments in this file. +# For advanced use and comprehensive documentation of the format, please see: +# https://docs.haskellstack.org/en/stable/configure/yaml/ + +# A 'specific' Stackage snapshot or a compiler version. +# A snapshot resolver dictates the compiler version and the set of packages +# to be used for project dependencies. For example: +# +# snapshot: lts-23.0 +# snapshot: nightly-2024-12-13 +# snapshot: ghc-9.8.4 +# +# The location of a snapshot can be provided as a file or url. Stack assumes +# a snapshot provided as a file might change, whereas a url resource does not. +# +# snapshot: ./custom-snapshot.yaml +# snapshot: https://example.com/snapshots/2024-01-01.yaml +# snapshot: +# url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/23/21.yaml + +snapshot: lts-23.27 + +# User packages to be built. +# Various formats can be used as shown in the example below. +# +# packages: +# - some-directory +# - https://example.com/foo/bar/baz-0.0.2.tar.gz +# subdirs: +# - auto-update +# - wai +packages: +- . +- ./Euterpea2 +- ./PortMidi +- ./vty-windows + +# Dependency packages to be pulled from upstream that are not in the snapshot. +# These entries can reference officially published versions as well as +# forks / in-progress versions pinned to a git hash. For example: +# +# extra-deps: +# - acme-missiles-0.3 +# - git: https://github.com/commercialhaskell/stack.git +# commit: e7b331f14bcffb8367cd58fbfc8b40ec7642100a +# +# extra-deps: [] + +extra-deps: +- Win32-2.14.2.1 +- directory-1.3.9.0 +- haskeline-0.8.3.0 +- process-1.6.26.1 +- time-1.12.2 + +# Override default flag values for project packages and extra-deps +# flags: [] + +# Extra package databases containing global packages +# extra-package-dbs: [] + +# Control whether we use the GHC we find on the path +# system-ghc: true +# +# Require a specific version of Stack, using version ranges +# require-stack-version: -any # Default +# require-stack-version: ">=3.3" +# +# Override the architecture used by Stack, especially useful on Windows +# arch: i386 +# arch: x86_64 +# +# Extra directories used by Stack for building +# extra-include-dirs: [/path/to/dir] +# extra-lib-dirs: [/path/to/dir] +# +# Allow a newer minor version of GHC than the snapshot specifies +# compiler-check: newer-minor diff --git a/vty-windows b/vty-windows new file mode 160000 index 0000000..6f37185 --- /dev/null +++ b/vty-windows @@ -0,0 +1 @@ +Subproject commit 6f3718597657b4d7b31c186683082202e9404519 From c99e2191541a4c4aaf7365de29bd649244f85862 Mon Sep 17 00:00:00 2001 From: yosukeueda33 Date: Fri, 1 Aug 2025 17:38:25 +0900 Subject: [PATCH 04/10] Add device selector on TUI. --- package.yaml | 5 +++ src/Tui.hs | 116 ++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 105 insertions(+), 16 deletions(-) diff --git a/package.yaml b/package.yaml index caf591f..4151a2f 100644 --- a/package.yaml +++ b/package.yaml @@ -38,6 +38,11 @@ dependencies: - filepath - directory - brick +- microlens +- microlens-th +- microlens-mtl +- vty +- vector ghc-options: - -Wall diff --git a/src/Tui.hs b/src/Tui.hs index 65eff0d..51d5a77 100644 --- a/src/Tui.hs +++ b/src/Tui.hs @@ -1,12 +1,32 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE TemplateHaskell #-} + module Tui where -import Control.Concurrent.MVar +import Control.Concurrent import Control.Concurrent.STM -import Control.Concurrent (forkIO, threadDelay) -import Brick (Widget, simpleMain, (<+>), str, withBorderStyle, joinBorders) -import Brick.Widgets.Center (center) -import Brick.Widgets.Border (borderWithLabel, vBorder) -import Brick.Widgets.Border.Style (unicode) +import qualified Data.List as L +import qualified Data.Vector as Vec +import Lens.Micro ((^.)) +import Lens.Micro.TH (makeLenses) +import Lens.Micro.Mtl +import Control.Monad (void) +import Control.Monad.Trans (liftIO) +#if !(MIN_VERSION_base(4,11,0)) +import Data.Monoid ((<>)) +#endif +import qualified Graphics.Vty as V + +import qualified Brick.Types as T +import Brick.AttrMap +import Brick.Util +import Brick.Types (Widget, ViewportType(Vertical)) +import qualified Brick.Main as M +import qualified Brick.Widgets.Edit as E +import qualified Brick.Widgets.Center as C +import qualified Brick.Widgets.Border as B +import Brick.Widgets.Core +import qualified Brick.Widgets.List as WL import Euterpea.IO.MIDI.MidiIO ( OutputDeviceID, InputDeviceID, unsafeInputID, unsafeOutputID ) @@ -19,17 +39,81 @@ data UiInput type UiUpdator = (Maybe String, Maybe Int, Maybe Int) -> IO () type DeviceLists = ([(InputDeviceID, String)], [(OutputDeviceID, String)]) +data Name = InputList | OutputList | SpecialList | StartStopButton + deriving (Eq, Ord, Show) + +data St = St { _chordName :: String + , _clockProgress :: Int + , _chordSetProgress :: Int + , _selectListIndex :: Int + , _inputList :: WL.List Name String + , _outputList :: WL.List Name String + , _specialList :: WL.List Name String + } + +makeLenses ''St -ui :: Widget () -ui = - joinBorders $ - withBorderStyle unicode $ - borderWithLabel (str "Hello!") - (center (str "Left") <+> vBorder <+> center (str "Right")) +drawUi :: St -> [Widget Name] +drawUi st = L.singleton + $ C.vCenterLayer $ + C.hCenterLayer $ + hBox [ + colHigh 0 (str "Select Input:") <=> + WL.renderList renderFunc True (st^.inputList) + , colHigh 1 (str "Select Output:") <=> + WL.renderList renderFunc True (st^.outputList) + , colHigh 2 (str "Select Special output:") <=> + WL.renderList renderFunc True (st^.specialList) + ] + where + colHigh i = if i == st^.selectListIndex then withAttr (attrName "colHighlight") else id + renderFunc sel e = rowHigh $ strWrap e + where rowHigh = if sel then withAttr (attrName "rowHighlight") else id + +appEvent :: T.BrickEvent Name e -> T.EventM Name St () +appEvent ev@(T.MouseDown n _ _ loc) = return () +appEvent (T.MouseUp {}) = return () +appEvent (T.VtyEvent (V.EvMouseUp {})) = return () +appEvent (T.VtyEvent (V.EvKey V.KLeft [])) = selectListIndex %= (\i -> max 0 (i - 1)) +appEvent (T.VtyEvent (V.EvKey V.KRight [])) = selectListIndex %= (\i -> min 2 (i + 1)) +appEvent (T.VtyEvent (V.EvKey V.KEsc [])) = M.halt +appEvent (T.VtyEvent ev) = do + nowCol <- use selectListIndex + let targetList = case nowCol of + 0 -> inputList + 1 -> outputList + 2 -> specialList + _ -> inputList + T.zoom targetList $ WL.handleListEvent ev +appEvent _ = return () -tuiMain :: MVar UiInput -> IO () -tuiMain mUiInput = do - _ <- simpleMain ui +aMap :: AttrMap +aMap = attrMap V.defAttr + [ (attrName "colHighlight", V.white `on` V.cyan) + , (attrName "rowHighlight", V.white `on` V.magenta) + ] + +app :: M.App St e Name +app = + M.App { M.appDraw = drawUi + , M.appChooseCursor = M.showFirstCursor + , M.appStartEvent = do + vty <- M.getVtyHandle + liftIO $ V.setMode (V.outputIface vty) V.Mouse True + , M.appHandleEvent = appEvent + , M.appAttrMap = const aMap + } + +tuiMain :: DeviceLists -> MVar UiInput -> IO () +tuiMain devices mUiInput = do + let + inputs = Vec.fromList . map (\(id, name) -> show id ++ ": " ++ name) $ fst devices + outputs = Vec.fromList . map (\(id, name) -> show id ++ ": "++ name) $ snd devices + specials = outputs + _ <- M.defaultMain app $ St "" 0 0 0 + (WL.list InputList inputs 5) + (WL.list OutputList outputs 5) + (WL.list SpecialList specials 5) putMVar mUiInput UiExit createTuiThread :: DeviceLists -> Bool -> IO (IO UiInput, UiUpdator) @@ -38,7 +122,7 @@ createTuiThread devices needOutSubDev = do tChordName' <- newTVarIO "" tClockProgress' <- newTVarIO 0 tChordSetProgress' <- newTVarIO 0 - _ <- forkIO $ tuiMain mUiInput + _ <- forkIO $ tuiMain devices mUiInput let uiInput = takeMVar mUiInput updateTVar tx = maybe (return ()) (atomically . writeTVar tx) From f8c414c913dce50671c1ea69803398a1e8c2215e Mon Sep 17 00:00:00 2001 From: yosukeueda33 Date: Fri, 1 Aug 2025 19:15:59 +0900 Subject: [PATCH 05/10] Add start/stop by space key on TUI. --- src/Tui.hs | 71 ++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 47 insertions(+), 24 deletions(-) diff --git a/src/Tui.hs b/src/Tui.hs index 51d5a77..7c57143 100644 --- a/src/Tui.hs +++ b/src/Tui.hs @@ -3,6 +3,8 @@ module Tui where +import Data.Bool (bool) +import Data.Maybe (fromJust) import Control.Concurrent import Control.Concurrent.STM import qualified Data.List as L @@ -49,6 +51,7 @@ data St = St { _chordName :: String , _inputList :: WL.List Name String , _outputList :: WL.List Name String , _specialList :: WL.List Name String + , _playing :: Bool } makeLenses ''St @@ -70,22 +73,32 @@ drawUi st = L.singleton renderFunc sel e = rowHigh $ strWrap e where rowHigh = if sel then withAttr (attrName "rowHighlight") else id -appEvent :: T.BrickEvent Name e -> T.EventM Name St () -appEvent ev@(T.MouseDown n _ _ loc) = return () -appEvent (T.MouseUp {}) = return () -appEvent (T.VtyEvent (V.EvMouseUp {})) = return () -appEvent (T.VtyEvent (V.EvKey V.KLeft [])) = selectListIndex %= (\i -> max 0 (i - 1)) -appEvent (T.VtyEvent (V.EvKey V.KRight [])) = selectListIndex %= (\i -> min 2 (i + 1)) -appEvent (T.VtyEvent (V.EvKey V.KEsc [])) = M.halt -appEvent (T.VtyEvent ev) = do - nowCol <- use selectListIndex - let targetList = case nowCol of - 0 -> inputList - 1 -> outputList - 2 -> specialList - _ -> inputList - T.zoom targetList $ WL.handleListEvent ev -appEvent _ = return () +appEvent :: (Int -> Int -> Int -> IO ()) + -> IO () -> T.BrickEvent Name e -> T.EventM Name St () +appEvent start stop (T.VtyEvent e) = case e of + (V.EvKey V.KLeft []) -> selectListIndex %= (\i -> max 0 (i - 1)) + (V.EvKey V.KRight []) -> selectListIndex %= (\i -> min 2 (i + 1)) + (V.EvKey (V.KChar ' ') []) -> + use playing >>= bool + (do + inId <- use $ inputList . WL.listSelectedL + outId <- use $ outputList . WL.listSelectedL + spId <- use $ specialList . WL.listSelectedL + liftIO $ start (fromJust inId) + (fromJust outId) + (fromJust spId) + playing %= const True) + (liftIO stop >> playing %= const False) + (V.EvKey V.KEsc []) -> M.halt + ev -> do + nowCol <- use selectListIndex + let targetList = case nowCol of + 0 -> inputList + 1 -> outputList + 2 -> specialList + _ -> inputList + T.zoom targetList $ WL.handleListEvent ev +appEvent _ _ _ = return () aMap :: AttrMap aMap = attrMap V.defAttr @@ -93,28 +106,30 @@ aMap = attrMap V.defAttr , (attrName "rowHighlight", V.white `on` V.magenta) ] -app :: M.App St e Name -app = +app :: (Int -> Int -> Int -> IO ()) -> IO () -> M.App St e Name +app start stop = M.App { M.appDraw = drawUi , M.appChooseCursor = M.showFirstCursor , M.appStartEvent = do vty <- M.getVtyHandle liftIO $ V.setMode (V.outputIface vty) V.Mouse True - , M.appHandleEvent = appEvent + , M.appHandleEvent = appEvent start stop , M.appAttrMap = const aMap } -tuiMain :: DeviceLists -> MVar UiInput -> IO () -tuiMain devices mUiInput = do +tuiMain :: DeviceLists + -> (Int -> Int -> Int -> IO ()) -> IO () -> IO () -> IO () +tuiMain devices start stop exit = do let inputs = Vec.fromList . map (\(id, name) -> show id ++ ": " ++ name) $ fst devices outputs = Vec.fromList . map (\(id, name) -> show id ++ ": "++ name) $ snd devices specials = outputs - _ <- M.defaultMain app $ St "" 0 0 0 + _ <- M.defaultMain (app start stop) $ St "" 0 0 0 (WL.list InputList inputs 5) (WL.list OutputList outputs 5) (WL.list SpecialList specials 5) - putMVar mUiInput UiExit + False + exit createTuiThread :: DeviceLists -> Bool -> IO (IO UiInput, UiUpdator) createTuiThread devices needOutSubDev = do @@ -122,7 +137,15 @@ createTuiThread devices needOutSubDev = do tChordName' <- newTVarIO "" tClockProgress' <- newTVarIO 0 tChordSetProgress' <- newTVarIO 0 - _ <- forkIO $ tuiMain devices mUiInput + _ <- forkIO $ tuiMain devices + (\inIx outIx spIx -> + let + inId = fst $ fst devices !! inIx + outId = fst $ snd devices !! outIx + mSpId = Just . fst $ snd devices !! spIx + in putMVar mUiInput $ UiStart inId outId mSpId) + (putMVar mUiInput UiStop) + (putMVar mUiInput UiExit) let uiInput = takeMVar mUiInput updateTVar tx = maybe (return ()) (atomically . writeTVar tx) From 0819e935c955dea9fa495fc396b3b3a1b9112909 Mon Sep 17 00:00:00 2001 From: yosukeueda33 Date: Sat, 2 Aug 2025 13:09:00 +0900 Subject: [PATCH 06/10] Disable prints that printed during TUI. --- src/Mapper.hs | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Mapper.hs b/src/Mapper.hs index 08e726e..1d77377 100644 --- a/src/Mapper.hs +++ b/src/Mapper.hs @@ -148,12 +148,12 @@ mapperMain = do --Start Midi threads. _ <- forkIO $ mainLoop loopExitSig loopExitDoneSig uiUpdator config inDev outDev mbOutSubDev - putStrLn "Loop Started." + -- putStrLn "Loop Started." loop -- Keep listening. UiStop -> do putMVar loopExitSig () takeMVar loopExitDoneSig - putStrLn "Loop Stop Done." + -- putStrLn "Loop Stop Done." putMVar loopExitDoneSig () loop -- Keep listening. UiExit -> do @@ -181,14 +181,14 @@ mainLoop exitSig exitDoneSig uiUpdator config inDev outDev mbOutSubDev = do tChordStep <- newTVarIO 0 preStopSig <- newTVarIO False stopSig <- newTVarIO False - putStrLn "Clearing MIDI Devices..." + -- putStrLn "Clearing MIDI Devices..." wait 0.5 (getRecData, recorder, getMsgToStop, recStart, recPlayResume, recPlayStop) <- createRecPlay tChordMap (fromIntegral $ recStepNum config) let qnSec = oneQnSec config op = do - putStrLn "Initializing MIDI Devices..." + -- putStrLn "Initializing MIDI Devices..." _ <- forkIO $ clockLoop qnSec tChordMapSet tChordMap genBuf preStopSig tChordStep uiUpdator (fromIntegral $ clockOffset config) @@ -208,19 +208,19 @@ mainLoop exitSig exitDoneSig uiUpdator config inDev outDev mbOutSubDev = do tPushingKeys tChordMap stopSig -- poll input and add to buffer _ <- forkIO $ controlReceiver stopSig tControl -- For Special Input [ ( NextChordMapSet - , putStrLn "Chord map change registered!" - >> changeChordMapSet + , -- putStrLn "Chord map change registered!" + changeChordMapSet tChordMapSet tChordMapSetIndex (chordMapSetList config)) , ( RecStart - , putStrLn "Rec Start!" - >> recStart) + , -- putStrLn "Rec Start!" + recStart) , ( RecPlayResume - , putStrLn "Rec Play Resume!" - >> recPlayResume) + , -- putStrLn "Rec Play Resume!" + recPlayResume) , ( RecPlayStop - , putStrLn "Rec Play Stop!" - >> recPlayStop + , -- putStrLn "Rec Play Stop!" + recPlayStop >> atomically ( readTVar tChordStep >>= getMsgToStop @@ -244,21 +244,21 @@ mainLoop exitSig exitDoneSig uiUpdator config inDev outDev mbOutSubDev = do return (length vals) mergedSendOut ellapsed = sum <$> mapM ($ ellapsed) [sendOut, sendSubOut] _ <- forkIO (midiOutRec 0.0 mergedSendOut stopSig) -- take from buffer and output - putStrLn "MIDI I/O services started." + -- putStrLn "MIDI I/O services started." detectExitLoop stopSig -- should only exit this via handleCtrlC closeOp = do atomically $ writeTVar preStopSig True -- signal the other threads to stop - putStrLn "Stopping clock signal" -- not clear why Ctrl+C is needed again + -- putStrLn "Stopping clock signal" -- not clear why Ctrl+C is needed again wait 1.0 atomically $ writeTVar stopSig True -- signal the other threads to stop - putStrLn "Stopping MIDI devices" -- not clear why Ctrl+C is needed again + -- putStrLn "Stopping MIDI devices" -- not clear why Ctrl+C is needed again wait 2.0 -- give the other threads time to stop before closing down MIDI! - putStrLn "before terminating..." + -- putStrLn "before terminating..." wait 0.5 -- give MIDI time to close down putMVar exitDoneSig () _ <- forkIO op takeMVar exitSig - putStrLn "Got exit" + -- putStrLn "Got exit" closeOp -- Invoke tasks by special inputs like tapping pads. @@ -370,7 +370,7 @@ clockLoop qnSec tChordMapSet tChordMap genBuf preStopSig tChordStep uiUpdator _ <- execStateT loop 0 sendStop uiUpdator (Just "", Just 0, Just 0) - putStrLn "closed clock" + -- putStrLn "closed clock" type ChordMap = (Int, String, ChordKeyMap) @@ -510,8 +510,8 @@ specialInput tControl cfgs msg = isSameNoteOn _ _ = False in when (isSameNoteOn cfgMsg msg) $ do - putStrLn "got special input." - print msg + -- putStrLn "got special input." + -- print msg atomically . writeTVar tControl $ Just control in mapM_ f cfgs From c9bfe04f27ff2bc9200382e3672fb6512bd2189f Mon Sep 17 00:00:00 2001 From: yosukeueda33 Date: Sat, 2 Aug 2025 13:21:31 +0900 Subject: [PATCH 07/10] Add bottom area on TUI. --- src/Tui.hs | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/Tui.hs b/src/Tui.hs index 7c57143..bbccf70 100644 --- a/src/Tui.hs +++ b/src/Tui.hs @@ -59,15 +59,19 @@ makeLenses ''St drawUi :: St -> [Widget Name] drawUi st = L.singleton $ C.vCenterLayer $ - C.hCenterLayer $ - hBox [ + C.hCenterLayer + (hBox [ colHigh 0 (str "Select Input:") <=> WL.renderList renderFunc True (st^.inputList) , colHigh 1 (str "Select Output:") <=> WL.renderList renderFunc True (st^.outputList) , colHigh 2 (str "Select Special output:") <=> WL.renderList renderFunc True (st^.specialList) - ] + ]) <=> + C.hCenterLayer (strWrap $ "Chord : " ++ st^.chordName ) <=> + C.hCenterLayer (strWrap $ "Chord set progress: " ++ concat (replicate (st^.chordSetProgress) "■")) <=> + C.hCenterLayer (strWrap $ "Chord progress : " ++ concat (replicate (st^.clockProgress) "⬤")) <=> + C.hCenterLayer (withAttr (attrName "info") $ strWrap "Left/Right: Select device type. Up/Down: Select device. Space: Start/Stop playing.") where colHigh i = if i == st^.selectListIndex then withAttr (attrName "colHighlight") else id renderFunc sel e = rowHigh $ strWrap e @@ -103,7 +107,8 @@ appEvent _ _ _ = return () aMap :: AttrMap aMap = attrMap V.defAttr [ (attrName "colHighlight", V.white `on` V.cyan) - , (attrName "rowHighlight", V.white `on` V.magenta) + , (attrName "rowHighlight", V.white `on` V.blue) + , (attrName "info", V.white `on` V.magenta) ] app :: (Int -> Int -> Int -> IO ()) -> IO () -> M.App St e Name @@ -124,11 +129,16 @@ tuiMain devices start stop exit = do inputs = Vec.fromList . map (\(id, name) -> show id ++ ": " ++ name) $ fst devices outputs = Vec.fromList . map (\(id, name) -> show id ++ ": "++ name) $ snd devices specials = outputs - _ <- M.defaultMain (app start stop) $ St "" 0 0 0 - (WL.list InputList inputs 5) - (WL.list OutputList outputs 5) - (WL.list SpecialList specials 5) - False + _ <- M.defaultMain (app start stop) + $ St { _chordName = "" + , _clockProgress = 0 + , _chordSetProgress = 0 + , _selectListIndex = 0 + , _inputList = WL.list InputList inputs 5 + , _outputList = WL.list OutputList outputs 5 + , _specialList = WL.list SpecialList specials 5 + , _playing = False + } exit createTuiThread :: DeviceLists -> Bool -> IO (IO UiInput, UiUpdator) From 4fbd06227f36ea7b50677da52ffe188c8daf006d Mon Sep 17 00:00:00 2001 From: yosukeueda33 Date: Sat, 2 Aug 2025 14:13:48 +0900 Subject: [PATCH 08/10] Add chord progress info reading on TUI. --- src/Tui.hs | 61 ++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/src/Tui.hs b/src/Tui.hs index bbccf70..8fbf4b2 100644 --- a/src/Tui.hs +++ b/src/Tui.hs @@ -31,6 +31,7 @@ import Brick.Widgets.Core import qualified Brick.Widgets.List as WL import Euterpea.IO.MIDI.MidiIO ( OutputDeviceID, InputDeviceID, unsafeInputID, unsafeOutputID ) +import Brick.BChan (BChan, newBChan, writeBChan) data UiInput = UiStart InputDeviceID OutputDeviceID @@ -38,6 +39,8 @@ data UiInput | UiStop | UiExit +data CustomEvent = ChangeChordInfo String Int Int + type UiUpdator = (Maybe String, Maybe Int, Maybe Int) -> IO () type DeviceLists = ([(InputDeviceID, String)], [(OutputDeviceID, String)]) @@ -68,17 +71,29 @@ drawUi st = L.singleton , colHigh 2 (str "Select Special output:") <=> WL.renderList renderFunc True (st^.specialList) ]) <=> - C.hCenterLayer (strWrap $ "Chord : " ++ st^.chordName ) <=> - C.hCenterLayer (strWrap $ "Chord set progress: " ++ concat (replicate (st^.chordSetProgress) "■")) <=> - C.hCenterLayer (strWrap $ "Chord progress : " ++ concat (replicate (st^.clockProgress) "⬤")) <=> - C.hCenterLayer (withAttr (attrName "info") $ strWrap "Left/Right: Select device type. Up/Down: Select device. Space: Start/Stop playing.") + C.hCenterLayer + (strWrap $ "Chord : " ++ st^.chordName ) <=> + C.hCenterLayer + (strWrap $ "Chord set progress: " + ++ concat (replicate (st^.chordSetProgress) "■")) <=> + C.hCenterLayer (strWrap $ "Chord progress : " + ++ concat (replicate (st^.clockProgress `div` 6) "⬤")) <=> + C.hCenterLayer + (withAttr (attrName "info") + . strWrap $ "Left/Right: Select device type." + ++ " Up/Down: Select device." + ++ " Space: Start/Stop playing.") where colHigh i = if i == st^.selectListIndex then withAttr (attrName "colHighlight") else id renderFunc sel e = rowHigh $ strWrap e where rowHigh = if sel then withAttr (attrName "rowHighlight") else id appEvent :: (Int -> Int -> Int -> IO ()) - -> IO () -> T.BrickEvent Name e -> T.EventM Name St () + -> IO () -> T.BrickEvent Name CustomEvent -> T.EventM Name St () +appEvent _ _ (T.AppEvent (ChangeChordInfo name clockI setI)) + = chordName %= const name + >> clockProgress %= const clockI + >> chordSetProgress %= const setI appEvent start stop (T.VtyEvent e) = case e of (V.EvKey V.KLeft []) -> selectListIndex %= (\i -> max 0 (i - 1)) (V.EvKey V.KRight []) -> selectListIndex %= (\i -> min 2 (i + 1)) @@ -111,7 +126,7 @@ aMap = attrMap V.defAttr , (attrName "info", V.white `on` V.magenta) ] -app :: (Int -> Int -> Int -> IO ()) -> IO () -> M.App St e Name +app :: (Int -> Int -> Int -> IO ()) -> IO () -> M.App St CustomEvent Name app start stop = M.App { M.appDraw = drawUi , M.appChooseCursor = M.showFirstCursor @@ -123,13 +138,15 @@ app start stop = } tuiMain :: DeviceLists - -> (Int -> Int -> Int -> IO ()) -> IO () -> IO () -> IO () -tuiMain devices start stop exit = do + -> (Int -> Int -> Int -> IO ()) -> IO () -> IO () + -> BChan CustomEvent + -> IO() +tuiMain devices start stop exit varsChan = do let inputs = Vec.fromList . map (\(id, name) -> show id ++ ": " ++ name) $ fst devices outputs = Vec.fromList . map (\(id, name) -> show id ++ ": "++ name) $ snd devices specials = outputs - _ <- M.defaultMain (app start stop) + _ <- M.customMainWithDefaultVty (Just varsChan) (app start stop) $ St { _chordName = "" , _clockProgress = 0 , _chordSetProgress = 0 @@ -141,12 +158,25 @@ tuiMain devices start stop exit = do } exit +readVars :: BChan CustomEvent -> TVar String -> TVar Int -> TVar Int -> IO () +readVars varsChan tChordName tClockProgress tChordSetProgress = go + where + go = do + threadDelay 100000 + writeBChan varsChan =<< ChangeChordInfo <$> readTVarIO tChordName + <*> readTVarIO tClockProgress + <*> readTVarIO tChordSetProgress + go + + createTuiThread :: DeviceLists -> Bool -> IO (IO UiInput, UiUpdator) createTuiThread devices needOutSubDev = do mUiInput <- newEmptyMVar - tChordName' <- newTVarIO "" - tClockProgress' <- newTVarIO 0 - tChordSetProgress' <- newTVarIO 0 + tChordName <- newTVarIO "" + tClockProgress <- newTVarIO 0 + tChordSetProgress <- newTVarIO 0 + varsChan <- newBChan 10 + void . forkIO $ readVars varsChan tChordName tClockProgress tChordSetProgress _ <- forkIO $ tuiMain devices (\inIx outIx spIx -> let @@ -156,12 +186,13 @@ createTuiThread devices needOutSubDev = do in putMVar mUiInput $ UiStart inId outId mSpId) (putMVar mUiInput UiStop) (putMVar mUiInput UiExit) + varsChan let uiInput = takeMVar mUiInput updateTVar tx = maybe (return ()) (atomically . writeTVar tx) updator (mbChordName, mbClockProgress, mbChordSetProgress) - = updateTVar tChordName' mbChordName - >> updateTVar tClockProgress' mbClockProgress - >> updateTVar tChordSetProgress' mbChordSetProgress + = updateTVar tChordName mbChordName + >> updateTVar tClockProgress mbClockProgress + >> updateTVar tChordSetProgress mbChordSetProgress return (uiInput, updator) From c842bb605263d5a1ac41a04f75d632bcc7ae3f27 Mon Sep 17 00:00:00 2001 From: yosukeueda33 Date: Sat, 2 Aug 2025 14:44:43 +0900 Subject: [PATCH 09/10] Clear shell when exit. --- src/Tui.hs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Tui.hs b/src/Tui.hs index 8fbf4b2..19ac4b7 100644 --- a/src/Tui.hs +++ b/src/Tui.hs @@ -108,7 +108,11 @@ appEvent start stop (T.VtyEvent e) = case e of (fromJust spId) playing %= const True) (liftIO stop >> playing %= const False) - (V.EvKey V.KEsc []) -> M.halt + (V.EvKey V.KEsc []) -> do + vty <- M.getVtyHandle + liftIO $ V.setMode (V.outputIface vty) V.Mouse False + liftIO $ putStr "\ESC[0m\ESC[2J\ESC[H" + M.halt ev -> do nowCol <- use selectListIndex let targetList = case nowCol of From 819b355cda1bc2e9e1afa564c5a93bec426fad6d Mon Sep 17 00:00:00 2001 From: yosukeueda33 Date: Sat, 2 Aug 2025 15:14:40 +0900 Subject: [PATCH 10/10] Remove unnecessaly layouting. --- src/Tui.hs | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/src/Tui.hs b/src/Tui.hs index 19ac4b7..303b254 100644 --- a/src/Tui.hs +++ b/src/Tui.hs @@ -61,28 +61,23 @@ makeLenses ''St drawUi :: St -> [Widget Name] drawUi st = L.singleton - $ C.vCenterLayer $ - C.hCenterLayer - (hBox [ - colHigh 0 (str "Select Input:") <=> - WL.renderList renderFunc True (st^.inputList) - , colHigh 1 (str "Select Output:") <=> - WL.renderList renderFunc True (st^.outputList) - , colHigh 2 (str "Select Special output:") <=> - WL.renderList renderFunc True (st^.specialList) - ]) <=> - C.hCenterLayer - (strWrap $ "Chord : " ++ st^.chordName ) <=> - C.hCenterLayer - (strWrap $ "Chord set progress: " - ++ concat (replicate (st^.chordSetProgress) "■")) <=> - C.hCenterLayer (strWrap $ "Chord progress : " - ++ concat (replicate (st^.clockProgress `div` 6) "⬤")) <=> - C.hCenterLayer - (withAttr (attrName "info") - . strWrap $ "Left/Right: Select device type." - ++ " Up/Down: Select device." - ++ " Space: Start/Stop playing.") + $ hBox [ + colHigh 0 (str "Select Input:") <=> + WL.renderList renderFunc True (st^.inputList) + , colHigh 1 (str "Select Output:") <=> + WL.renderList renderFunc True (st^.outputList) + , colHigh 2 (str "Select Special output:") <=> + WL.renderList renderFunc True (st^.specialList) + ] <=> + strWrap ( "Chord : " ++ st^.chordName ) <=> + strWrap ( "Chord set progress: " + ++ concat (replicate (st^.chordSetProgress) "o")) <=> + strWrap ( "Chord progress : " + ++ concat (replicate (st^.clockProgress `div` 6) "o")) <=> + (withAttr (attrName "info") + . strWrap $ "Left/Right: Select device type." + ++ " Up/Down: Select device." + ++ " Space: Start/Stop playing.") where colHigh i = if i == st^.selectListIndex then withAttr (attrName "colHighlight") else id renderFunc sel e = rowHigh $ strWrap e