Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
8bb30d4ed3 |
|||
|
c6f5a8fd39 |
|||
|
eb59d860ef |
|||
|
197c806412 |
|||
|
69b12db443 |
|||
|
1e3e93507f |
|||
|
6bd7f81c1b |
|||
|
874ece8deb |
|||
|
6f9fa07fac |
|||
|
170bc26cd4 |
|||
|
8e89fc621e |
|||
|
bbe2bf2cef |
|||
|
2277a8a59f |
|||
| 40bb31e9a8 | |||
| 13b81bc1ce | |||
| 5ba10e7cdd | |||
| c128117693 | |||
| 76f1aafa8e | |||
| 00eab63c75 | |||
| 855db174b1 | |||
| 774e42f434 |
7 changed files with 396 additions and 199 deletions
12
.gitignore
vendored
Normal file
12
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# .gitignore
|
||||||
|
|
||||||
|
|
||||||
|
*~
|
||||||
|
*#
|
||||||
|
.#*
|
||||||
|
*rej
|
||||||
|
*orig
|
||||||
|
pinentry*
|
||||||
|
*.hi
|
||||||
|
*.o
|
||||||
|
*.a
|
||||||
23
README.md
23
README.md
|
|
@ -5,26 +5,9 @@ wazajisho library for handling japanese text in haskell
|
||||||
<img src="https://git.ajattix.org/hashirama/waza-hs/raw/branch/main/misc/cover.jpg" alt="cover" width="30%" height="30%">
|
<img src="https://git.ajattix.org/hashirama/waza-hs/raw/branch/main/misc/cover.jpg" alt="cover" width="30%" height="30%">
|
||||||
|
|
||||||
|
|
||||||
### FileUtils.h
|
|
||||||
|
|
||||||
for managing dictionaries we have a custom data structure, and you can use it that way:
|
### usage:
|
||||||
|
``` shell
|
||||||
```haskell
|
$ ghc -o myprog examples/Main.hs -i./src
|
||||||
-- Access all directories in the list of FileGroups
|
|
||||||
let directories = map directory fileGroups
|
|
||||||
print directories -- ["/mnt/data1", "/mnt/data2"]
|
|
||||||
|
|
||||||
-- Access all file lists in the list of FileGroups
|
|
||||||
let filesList = map files fileGroups
|
|
||||||
print filesList -- [["file1.mdx", "file2.css"], ["file3.mdx", "file4.png"]]
|
|
||||||
|
|
||||||
-- Filter the FileGroups where any file in the files list contains ".mdx"
|
|
||||||
let mdict_filter = filter (\group -> any (isSuffixOf ".mdx") (files group)) fileGroups
|
|
||||||
|
|
||||||
-- Access all files of the filtered FileGroups
|
|
||||||
let mdict_files = map files mdict_files
|
|
||||||
```
|
```
|
||||||
|
|
||||||
you can for example, pass that list as a parameter for loading all of those dictionaries in [mdict-cpp](https://github.com/dictlab/mdict-cpp)
|
|
||||||
|
|
||||||
TODO: move those functions to be some kind of helpers or operators
|
|
||||||
133
examples/Main.hs
133
examples/Main.hs
|
|
@ -1,7 +1,132 @@
|
||||||
import KanaConv (convertHalfToFull)
|
-- Copyright © 2025 Hashirama Senju
|
||||||
|
{-# LANGUAGE ImplicitParams #-}
|
||||||
|
{-# LANGUAGE OverloadedLabels #-}
|
||||||
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE BlockArguments #-}
|
||||||
|
|
||||||
|
import GI.Gtk
|
||||||
|
import qualified GI.Gtk as Gtk
|
||||||
|
import GI.GLib (mainContextInvokeFull, MainContext)
|
||||||
|
import GI.Gio hiding (Application)
|
||||||
|
import Wazahs
|
||||||
|
import Control.Concurrent
|
||||||
|
import System.FilePath ((</>))
|
||||||
|
import System.IO (writeFile)
|
||||||
|
import Control.Concurrent.STM
|
||||||
|
import System.Directory
|
||||||
|
import qualified GI.WebKit.Objects.WebView as WK
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Text.IO as TIO
|
||||||
|
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
let input = "おいそこのキミ。あっはいはいはいは~い。"
|
app <- new Application []
|
||||||
let output = convertHalfToFull input
|
|
||||||
putStrLn output
|
onApplicationActivate app $ do
|
||||||
|
window <- new GI.Gtk.ApplicationWindow [#application := app, #title := "Wazajisho"]
|
||||||
|
fd <- Gtk.fileDialogNew
|
||||||
|
|
||||||
|
home <- getHomeDirectory
|
||||||
|
let cfg = home </> ".local" </> "share" </> "wazajisho" </> "config"
|
||||||
|
|
||||||
|
filesVar <- newTVarIO []
|
||||||
|
|
||||||
|
getFolderAsync fd window cfg $ \mPath -> case mPath of
|
||||||
|
Nothing -> putStrLn "Callback: no folder selected"
|
||||||
|
Just dir -> do
|
||||||
|
putStrLn $ "Callback: folder selected: " ++ dir
|
||||||
|
fs <- listFiles dir
|
||||||
|
putStrLn $ "Callback: listFiles found: " ++ show fs
|
||||||
|
atomically $ do
|
||||||
|
writeTVar filesVar fs
|
||||||
|
|
||||||
|
|
||||||
|
rootVBox <- Gtk.boxNew Gtk.OrientationVertical 6
|
||||||
|
topHBox <- Gtk.boxNew Gtk.OrientationHorizontal 6
|
||||||
|
Gtk.widgetSetHexpand topHBox True
|
||||||
|
mainBox <- Gtk.boxNew Gtk.OrientationVertical 0
|
||||||
|
|
||||||
|
-- forces code to run in the same thread as gtk
|
||||||
|
mainContextInvokeFull Nothing 0 $ do
|
||||||
|
currentFiles <- readTVarIO filesVar
|
||||||
|
let icons = findIcons currentFiles
|
||||||
|
putStrLn "this was executed" -- well, it does works.
|
||||||
|
mapM_ putStrLn icons
|
||||||
|
toolbar <- createIconToolbar icons
|
||||||
|
Gtk.boxAppend mainBox toolbar
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
let htmlFilePath = "/tmp/index.html"
|
||||||
|
content <- TIO.readFile htmlFilePath
|
||||||
|
webview <- WK.webViewNew
|
||||||
|
|
||||||
|
-- Load HTML content
|
||||||
|
WK.webViewLoadHtml webview content (Nothing :: Maybe T.Text)
|
||||||
|
|
||||||
|
-- Main vertical box
|
||||||
|
|
||||||
|
|
||||||
|
-- Toolbar
|
||||||
|
|
||||||
|
|
||||||
|
-- Search bar
|
||||||
|
searchBar <- new Gtk.Box
|
||||||
|
[ #orientation := Gtk.OrientationHorizontal
|
||||||
|
, #spacing := 20
|
||||||
|
, #baselinePosition := Gtk.BaselinePositionTop
|
||||||
|
, #halign := Gtk.AlignFill
|
||||||
|
, #hexpand := True
|
||||||
|
, #widthRequest := 300
|
||||||
|
, #marginTop := 10
|
||||||
|
, #marginEnd := 10
|
||||||
|
, #marginStart := 15
|
||||||
|
]
|
||||||
|
|
||||||
|
entry <- Gtk.entryNew
|
||||||
|
Gtk.editableSetEditable entry True
|
||||||
|
#setHexpand entry True
|
||||||
|
|
||||||
|
searchButton <- new Gtk.Button [#label := "Search"]
|
||||||
|
|
||||||
|
Gtk.boxAppend searchBar entry
|
||||||
|
Gtk.boxAppend searchBar searchButton
|
||||||
|
Gtk.boxAppend mainBox searchBar
|
||||||
|
|
||||||
|
-- Phrase label (Yomichan-style heading)
|
||||||
|
phraseLabel <- new Gtk.Label [ #label := ("日本語の例文です" :: T.Text) ]
|
||||||
|
Gtk.widgetSetHalign phraseLabel Gtk.AlignCenter
|
||||||
|
Gtk.widgetSetHexpand phraseLabel True
|
||||||
|
#addCssClass phraseLabel "title-1" -- large font style
|
||||||
|
Gtk.boxAppend mainBox phraseLabel
|
||||||
|
|
||||||
|
-- Webview
|
||||||
|
webviewBox <- Gtk.boxNew Gtk.OrientationHorizontal 0
|
||||||
|
Gtk.boxAppend webviewBox webview
|
||||||
|
Gtk.widgetSetVexpand webview True
|
||||||
|
Gtk.widgetSetHexpand webview True
|
||||||
|
Gtk.boxAppend mainBox webviewBox
|
||||||
|
|
||||||
|
|
||||||
|
-- Gtk.boxAppend topHBox searchBar -- search bar in top area
|
||||||
|
|
||||||
|
-- keep your existing mainBox as the content area
|
||||||
|
Gtk.boxAppend rootVBox topHBox
|
||||||
|
Gtk.boxAppend rootVBox mainBox
|
||||||
|
|
||||||
|
-- now set the window child to rootVBox (do this once)
|
||||||
|
Gtk.windowSetChild window (Just rootVBox)
|
||||||
|
|
||||||
|
-- show
|
||||||
|
Gtk.widgetShow topHBox
|
||||||
|
|
||||||
|
Gtk.widgetShow rootVBox
|
||||||
|
Gtk.widgetShow window
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
applicationRun app Nothing
|
||||||
|
return ()
|
||||||
|
|
|
||||||
127
src/FileUtils.hs
127
src/FileUtils.hs
|
|
@ -1,127 +0,0 @@
|
||||||
-- Copyright © 2024 Hashirama Senju
|
|
||||||
|
|
||||||
import System.Clock -- To measure time
|
|
||||||
import System.Console.ANSI -- For colored output
|
|
||||||
--
|
|
||||||
import Z.Data.CBytes (pack, unpack)
|
|
||||||
import Z.IO.FileSystem (scandirRecursively)
|
|
||||||
import System.FilePath (takeDirectory)
|
|
||||||
import Data.List (groupBy, isSuffixOf, nub)
|
|
||||||
import System.IO
|
|
||||||
import qualified Data.Map as Map
|
|
||||||
import Control.Concurrent.Async (mapConcurrently)
|
|
||||||
import Control.Concurrent
|
|
||||||
|
|
||||||
-- Derived version with automatic string handling
|
|
||||||
scanDirectoryRecursivelyStr :: String -> IO [String]
|
|
||||||
scanDirectoryRecursivelyStr path = do
|
|
||||||
result <- scandirRecursively (pack path) (\p _ -> return True)
|
|
||||||
return (map unpack result) -- Ensures all results are decoded to Strings (UTF-8)
|
|
||||||
|
|
||||||
-- Function to filter files with a specific extension
|
|
||||||
filterMdx :: [String] -> String -> [String]
|
|
||||||
filterMdx files ext = filter (isSuffixOf ext) files
|
|
||||||
|
|
||||||
-- Custom implementation of takeDirectory for UTF-8 strings
|
|
||||||
takeDirectory' :: String -> String
|
|
||||||
takeDirectory' path = reverse . dropWhile (/= '/') . reverse $ path
|
|
||||||
|
|
||||||
data FileGroup = FileGroup
|
|
||||||
{ directory :: String -- The directory part of the file path (UTF-8 String)
|
|
||||||
, files :: [String] -- List of file paths in that directory (UTF-8 Strings)
|
|
||||||
} deriving (Show, Eq)
|
|
||||||
|
|
||||||
-- Create a map from directory to list of .mdx files
|
|
||||||
groupMdxByDirectory :: [String] -> Map.Map String [String]
|
|
||||||
groupMdxByDirectory mdxFiles =
|
|
||||||
Map.fromListWith (++) [(takeDirectory' file, [file]) | file <- mdxFiles]
|
|
||||||
|
|
||||||
-- Group files by their directory, including additional files that belong to the same directory
|
|
||||||
groupFilesByDirectory :: [String] -> [String] -> IO [FileGroup]
|
|
||||||
groupFilesByDirectory mdxFiles additionalFiles = do
|
|
||||||
-- Create a map of directories to .mdx files
|
|
||||||
let mdxMap = groupMdxByDirectory mdxFiles
|
|
||||||
|
|
||||||
-- Function to process each directory group in parallel
|
|
||||||
let processGroup dir mdxFilesInDir = do
|
|
||||||
let -- Filter additional files that belong to the same directory
|
|
||||||
additionalFilesInDir = filter (\f -> takeDirectory' f == dir) additionalFiles
|
|
||||||
-- Remove matching files from additional files (those that are already in the .mdx list)
|
|
||||||
remainingAdditionalFiles = filter (\f -> not (f `elem` mdxFilesInDir)) additionalFilesInDir
|
|
||||||
-- Combine and remove duplicates
|
|
||||||
allFiles = nub (mdxFilesInDir ++ remainingAdditionalFiles)
|
|
||||||
return $ FileGroup dir allFiles
|
|
||||||
|
|
||||||
-- Process each directory group concurrently
|
|
||||||
groupedFiles <- mapConcurrently (\(dir, mdxFilesInDir) -> processGroup dir mdxFilesInDir) (Map.toList mdxMap)
|
|
||||||
return groupedFiles
|
|
||||||
|
|
||||||
-- Function to convert FileGroup to a string representation for file output
|
|
||||||
fileGroupToString :: FileGroup -> String
|
|
||||||
fileGroupToString (FileGroup dir files) =
|
|
||||||
"Directory: " ++ dir ++ "\n" ++
|
|
||||||
unlines (map (" " ++) files)
|
|
||||||
|
|
||||||
|
|
||||||
-- Box-like animation function with green color
|
|
||||||
boxAnimation :: IO ()
|
|
||||||
boxAnimation = do
|
|
||||||
let frames = ["[ ]", "[. ]", "[ . ]", "[ .]"] -- Frame progression for box
|
|
||||||
let loop frames' = do
|
|
||||||
mapM_ (\frame -> do
|
|
||||||
setSGR [SetColor Foreground Vivid Green] -- Set the box color to green
|
|
||||||
putStr "\r" -- Move cursor to the start of the line
|
|
||||||
putStr frame -- Print the current frame
|
|
||||||
hFlush stdout -- Flush output buffer
|
|
||||||
threadDelay 200000) frames' -- Wait 0.2 seconds between frames
|
|
||||||
loop frames' -- Recursively call to keep the loop running
|
|
||||||
loop frames -- Start the looping animation
|
|
||||||
|
|
||||||
|
|
||||||
-- Main function
|
|
||||||
main :: IO ()
|
|
||||||
main = do
|
|
||||||
|
|
||||||
-- Start timing the operation
|
|
||||||
start <- getTime Monotonic
|
|
||||||
|
|
||||||
-- Notify the start of processing with color
|
|
||||||
setSGR [SetColor Foreground Vivid Blue]
|
|
||||||
putStrLn "Processing started..."
|
|
||||||
setSGR [Reset] -- Reset colors
|
|
||||||
-- Create a thread for the box animation
|
|
||||||
animationThread <- forkIO boxAnimation
|
|
||||||
|
|
||||||
|
|
||||||
allFiles <- scanDirectoryRecursivelyStr "/mnt/Data/Japanese_Resources/"
|
|
||||||
|
|
||||||
-- Filter files with '.mdx' extension
|
|
||||||
let mdxFiles = filterMdx allFiles ".mdx"
|
|
||||||
|
|
||||||
-- Group the files by their directories concurrently
|
|
||||||
groupedFiles <- groupFilesByDirectory mdxFiles allFiles
|
|
||||||
|
|
||||||
-- just to show that we finished processing, might be faster than doing I/O
|
|
||||||
putStrLn "Processing complete. Data is grouped."
|
|
||||||
-- Kill the animation thread
|
|
||||||
killThread animationThread
|
|
||||||
-- Stop timing the operation
|
|
||||||
end <- getTime Monotonic
|
|
||||||
let elapsed = toNanoSecs (diffTimeSpec end start) `div` 1000000 -- Convert to milliseconds
|
|
||||||
|
|
||||||
-- Show a colorful message after processing with elapsed time
|
|
||||||
setSGR [SetColor Foreground Vivid Green]
|
|
||||||
putStrLn $ "Processing complete. Time taken: " ++ show elapsed ++ " ms."
|
|
||||||
setSGR [Reset] -- Reset colors
|
|
||||||
|
|
||||||
putStrLn "Starting to write results..."
|
|
||||||
animationThread <- forkIO boxAnimation
|
|
||||||
-- Open the output file and set its encoding to UTF-8
|
|
||||||
withFile "output.txt" WriteMode $ \handle -> do
|
|
||||||
hSetEncoding handle utf8
|
|
||||||
|
|
||||||
-- Write the grouped files to the file
|
|
||||||
mapM_ (hPutStrLn handle . fileGroupToString) groupedFiles
|
|
||||||
|
|
||||||
killThread animationThread
|
|
||||||
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
-- Copyright © 2024 Hashirama Senju
|
|
||||||
|
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
|
|
||||||
module KanaConv (convertHalfToFull) where -- Export convertHalfToFull function
|
|
||||||
|
|
||||||
import qualified Data.Map as Map
|
|
||||||
import Data.Maybe (fromMaybe)
|
|
||||||
|
|
||||||
-- The Kana mapping for half-width to full-width katakana
|
|
||||||
halfToFullMapping :: Map.Map String String
|
|
||||||
halfToFullMapping = Map.fromList
|
|
||||||
[ ("ア", "ア"), ("イ", "イ"), ("ウ", "ウ"), ("エ", "エ"), ("オ", "オ")
|
|
||||||
, ("カ", "カ"), ("キ", "キ"), ("ク", "ク"), ("ケ", "ケ"), ("コ", "コ")
|
|
||||||
, ("サ", "サ"), ("シ", "シ"), ("ス", "ス"), ("セ", "セ"), ("ソ", "ソ")
|
|
||||||
, ("タ", "タ"), ("チ", "チ"), ("ツ", "ツ"), ("テ", "テ"), ("ト", "ト")
|
|
||||||
, ("ナ", "ナ"), ("ニ", "ニ"), ("ヌ", "ヌ"), ("ネ", "ネ"), ("ノ", "ノ")
|
|
||||||
, ("ハ", "ハ"), ("ヒ", "ヒ"), ("フ", "フ"), ("ヘ", "ヘ"), ("ホ", "ホ")
|
|
||||||
, ("マ", "マ"), ("ミ", "ミ"), ("ム", "ム"), ("メ", "メ"), ("モ", "モ")
|
|
||||||
, ("ᄂ", "ヤ"), ("ᆭ", "ユ"), ("ᄃ", "ヨ"), ("ᄄ", "ラ"), ("ᄅ", "リ")
|
|
||||||
, ("ᆰ", "ル"), ("ᆲ", "レ"), ("ᆴ", "ロ"), ("ワ", "ワ"), ("ヲ", "ヲ")
|
|
||||||
, ("ン", "ン"), ("ァ", "ァ"), ("ィ", "ィ"), ("ゥ", "ゥ"), ("ェ", "ェ")
|
|
||||||
, ("ォ", "ォ"), ("ッ", "ッ"), ("ャ", "ャ"), ("ュ", "ュ"), ("ョ", "ョ")
|
|
||||||
, ("。", "。"), ("、", "、"), ("・", "・"), ("゛", "゙"), ("゜", "゚")
|
|
||||||
, ("「", "「"), ("」", "」"), ("ー", "ー")
|
|
||||||
]
|
|
||||||
|
|
||||||
-- Function to replace half-width characters with full-width characters
|
|
||||||
replaceChar :: Char -> String
|
|
||||||
replaceChar char
|
|
||||||
| Just full <- Map.lookup [char] halfToFullMapping = full
|
|
||||||
| otherwise = [char]
|
|
||||||
|
|
||||||
-- Function to convert the entire string
|
|
||||||
convertHalfToFull :: String -> String
|
|
||||||
convertHalfToFull = concatMap replaceChar
|
|
||||||
|
|
@ -1,12 +0,0 @@
|
||||||
import Data.Text.Lazy as TL
|
|
||||||
|
|
||||||
parseKB :: [String] -> [String]
|
|
||||||
parseKB keylist = do {-
|
|
||||||
the thing is, we need to process this as a list, maybe with Map
|
|
||||||
-}
|
|
||||||
our_keylist = TL.pack keylist
|
|
||||||
our_operator = TL.pack "-"
|
|
||||||
splited = TL.unpack $ TL.splitOn our_operator our_keylist
|
|
||||||
|
|
||||||
-- getting the keys is partially implemented here:
|
|
||||||
-- https://git.ajattix.org/hashirama/haskell-examples/src/branch/main/keyPress.hs
|
|
||||||
252
src/Wazahs.hs
Normal file
252
src/Wazahs.hs
Normal file
|
|
@ -0,0 +1,252 @@
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE OverloadedLabels #-}
|
||||||
|
{-# LANGUAGE ImplicitParams #-}
|
||||||
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
|
|
||||||
|
module Wazahs (
|
||||||
|
listFiles,
|
||||||
|
selectFolder,
|
||||||
|
getFolderAsync,
|
||||||
|
createIconToolbar,
|
||||||
|
findIcons,
|
||||||
|
(<@),
|
||||||
|
attachDialogOnRealize
|
||||||
|
) where
|
||||||
|
|
||||||
|
-- imports
|
||||||
|
|
||||||
|
import qualified Data.Conduit.Combinators as CC
|
||||||
|
import GI.Gtk
|
||||||
|
import GI.GLib (mainContextInvokeFull)
|
||||||
|
import GI.Gio hiding (Application)
|
||||||
|
import qualified GI.Gtk as GTK
|
||||||
|
import qualified GI.Gtk as Gtk
|
||||||
|
import qualified GI.Gdk as Gdk
|
||||||
|
import qualified GI.GdkX11 as GdkX11
|
||||||
|
import Data.GI.Base.Signals
|
||||||
|
import Data.GI.Base (castTo)
|
||||||
|
import Foreign.Marshal.Array (withArray)
|
||||||
|
import Foreign.Ptr (castPtr)
|
||||||
|
import Foreign.C.Types (CULong)
|
||||||
|
import Graphics.X11.Xlib
|
||||||
|
import Graphics.X11.Xlib.Extras (xChangeProperty, propModeReplace)
|
||||||
|
import Control.Monad (when, void, mzero, guard)
|
||||||
|
import Data.Foldable (for_)
|
||||||
|
import System.Directory
|
||||||
|
import System.FilePath
|
||||||
|
import Control.Monad.IO.Class (liftIO)
|
||||||
|
import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
|
||||||
|
import Data.Conduit
|
||||||
|
import System.Environment
|
||||||
|
import Control.Concurrent
|
||||||
|
import System.IO (writeFile)
|
||||||
|
import Conduit
|
||||||
|
import System.Process
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Data.Map as M
|
||||||
|
import System.Exit
|
||||||
|
|
||||||
|
-- | Recursively list all files in the given directory with .mdx extension.
|
||||||
|
listFiles :: FilePath -> IO [FilePath]
|
||||||
|
listFiles dir = runConduitRes $
|
||||||
|
CC.sourceDirectoryDeep False dir
|
||||||
|
.| filterC (\fp -> takeExtension fp == ".mdx")
|
||||||
|
.| sinkList
|
||||||
|
|
||||||
|
-- | Helper function for selecting folders in gtk
|
||||||
|
selectFolder :: FileDialog -> ApplicationWindow -> (Maybe String -> IO ()) -> IO ()
|
||||||
|
selectFolder fd w k =
|
||||||
|
fileDialogSelectFolder fd (Just w) (Nothing :: Maybe Cancellable) $ Just $ \_ r -> do
|
||||||
|
mbPath <- runMaybeT $ do
|
||||||
|
f <- MaybeT $ handleDialogError (\_ _ -> pure Nothing) (fileDialogSelectFolderFinish fd r)
|
||||||
|
p <- MaybeT $ fileGetPath f
|
||||||
|
p <$ (liftIO (doesDirectoryExist p) >>= guard)
|
||||||
|
k mbPath
|
||||||
|
|
||||||
|
-- Blocking: returns Maybe FilePath (checks config first).
|
||||||
|
-- WARNING: don't call on GTK main thread (it will block the dialog).
|
||||||
|
getFolderBlocking
|
||||||
|
:: FileDialog -- dialog (created by caller)
|
||||||
|
-> ApplicationWindow -- parent (created by caller)
|
||||||
|
-> FilePath -- config file path
|
||||||
|
-> IO (Maybe FilePath)
|
||||||
|
getFolderBlocking fd w configFile = do
|
||||||
|
-- check config
|
||||||
|
cfgOk <- doesFileExist configFile
|
||||||
|
if cfgOk
|
||||||
|
then do
|
||||||
|
path <- readFile configFile
|
||||||
|
ok <- doesDirectoryExist path
|
||||||
|
if ok then return (Just path) else prompt
|
||||||
|
else prompt
|
||||||
|
where
|
||||||
|
prompt = do
|
||||||
|
mv <- newEmptyMVar
|
||||||
|
-- use the existing callback-style selectFolder
|
||||||
|
selectFolder fd w $ \mPath -> do
|
||||||
|
case mPath of
|
||||||
|
Nothing -> putMVar mv Nothing
|
||||||
|
Just p -> do
|
||||||
|
createDirectoryIfMissing True (takeDirectory configFile)
|
||||||
|
writeFile configFile p
|
||||||
|
putMVar mv (Just p)
|
||||||
|
takeMVar mv
|
||||||
|
|
||||||
|
|
||||||
|
-- | Restart the current program
|
||||||
|
restartProgram :: IO ()
|
||||||
|
restartProgram = do
|
||||||
|
exePath <- getExecutablePath
|
||||||
|
_ <- spawnProcess exePath [] -- launch a new instance
|
||||||
|
exitSuccess -- exit this instance
|
||||||
|
|
||||||
|
|
||||||
|
-- Async: safe to call from GTK main thread. Calls k with result.
|
||||||
|
getFolderAsync
|
||||||
|
:: FileDialog
|
||||||
|
-> ApplicationWindow
|
||||||
|
-> FilePath
|
||||||
|
-> (Maybe FilePath -> IO ()) -- callback to receive result
|
||||||
|
-> IO ()
|
||||||
|
getFolderAsync fd w configFile k = do
|
||||||
|
cfgOk <- doesFileExist configFile
|
||||||
|
if cfgOk
|
||||||
|
then do
|
||||||
|
path <- readFile configFile
|
||||||
|
ok <- doesDirectoryExist path
|
||||||
|
if ok
|
||||||
|
then k (Just path)
|
||||||
|
else showDialog
|
||||||
|
else showDialog
|
||||||
|
where
|
||||||
|
showDialog = selectFolder fd w $ \mPath -> case mPath of
|
||||||
|
Nothing -> k Nothing
|
||||||
|
Just p -> do
|
||||||
|
createDirectoryIfMissing True (takeDirectory configFile)
|
||||||
|
writeFile configFile p
|
||||||
|
k (Just p)
|
||||||
|
restartProgram
|
||||||
|
|
||||||
|
|
||||||
|
-- Return all icons corresponding to .mdx files
|
||||||
|
findIcons :: [FilePath] -> [FilePath]
|
||||||
|
findIcons files = [replaceExtension f "png" | f <- files, takeExtension f == ".mdx"]
|
||||||
|
|
||||||
|
|
||||||
|
-- not related to file handling:
|
||||||
|
createIconToolbar' :: [FilePath] -> IO Gtk.Box
|
||||||
|
createIconToolbar' iconPaths = do
|
||||||
|
toolbar <- Gtk.boxNew Gtk.OrientationHorizontal 0
|
||||||
|
Gtk.widgetSetHalign toolbar Gtk.AlignCenter
|
||||||
|
Gtk.widgetSetValign toolbar Gtk.AlignCenter
|
||||||
|
|
||||||
|
cssProvider <- Gtk.cssProviderNew
|
||||||
|
Gtk.cssProviderLoadFromString cssProvider $ T.pack $
|
||||||
|
".classic-button {"
|
||||||
|
++ " -gtk-appearance: none;"
|
||||||
|
++ " background-image: linear-gradient(to bottom, #f3f3f3, #d8d8d8);"
|
||||||
|
++ " border: 1px solid #bbb;"
|
||||||
|
++ " border-radius: 4px;"
|
||||||
|
++ " color: #333;"
|
||||||
|
++ " box-shadow: inset 0 1px 0 rgba(255,255,255,0.7);"
|
||||||
|
++ " padding: 4px 8px;"
|
||||||
|
++ "}"
|
||||||
|
++ ".classic-button:hover {"
|
||||||
|
++ " background-image: linear-gradient(to bottom, #e6e6e6, #ccc);"
|
||||||
|
++ "}"
|
||||||
|
++ ".classic-button:active {"
|
||||||
|
++ " background-image: linear-gradient(to bottom, #ccc, #e6e6e6);"
|
||||||
|
++ "}"
|
||||||
|
|
||||||
|
|
||||||
|
mapM_ (\path -> do
|
||||||
|
icon <- Gtk.imageNewFromFile path
|
||||||
|
Gtk.widgetSetSizeRequest icon 64 64 -- Force minimum size
|
||||||
|
|
||||||
|
btn <- Gtk.buttonNew
|
||||||
|
Gtk.widgetAddCssClass btn "glass-button"
|
||||||
|
Gtk.widgetSetSizeRequest btn 80 80 -- Button size (icon + padding)
|
||||||
|
Gtk.widgetSetHalign btn Gtk.AlignCenter
|
||||||
|
Gtk.widgetSetValign btn Gtk.AlignCenter
|
||||||
|
Gtk.buttonSetChild btn (Just icon)
|
||||||
|
|
||||||
|
-- Add click handler
|
||||||
|
void $ Gtk.on btn #clicked $ do
|
||||||
|
putStrLn $ "Selected: " ++ path
|
||||||
|
-- Add your click handler logic here
|
||||||
|
|
||||||
|
Gtk.boxAppend toolbar btn
|
||||||
|
) iconPaths
|
||||||
|
|
||||||
|
return toolbar
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Wrap the toolbar in a ScrolledWindow that scrolls horizontally
|
||||||
|
createIconToolbar :: [FilePath] -> IO Gtk.Widget
|
||||||
|
createIconToolbar iconPaths = do
|
||||||
|
-- Get your original toolbar (horizontal box)
|
||||||
|
toolbar <- createIconToolbar' iconPaths
|
||||||
|
|
||||||
|
-- Create a new scrolled window
|
||||||
|
scrolled <- Gtk.scrolledWindowNew
|
||||||
|
|
||||||
|
-- Show the horizontal scrollbar automatically, never show the vertical one
|
||||||
|
Gtk.scrolledWindowSetPolicy scrolled Gtk.PolicyTypeAutomatic Gtk.PolicyTypeNever
|
||||||
|
|
||||||
|
-- Put the horizontal box (your toolbar) inside the scrolled window
|
||||||
|
Gtk.scrolledWindowSetChild scrolled (Just toolbar)
|
||||||
|
|
||||||
|
-- Return as a general Widget
|
||||||
|
Gtk.toWidget scrolled
|
||||||
|
|
||||||
|
-- helper for forcing lambdas to run in the gtk thread:
|
||||||
|
infixr 0 <@
|
||||||
|
(<@) :: IO () -> IO ()
|
||||||
|
(<@) action = mainContextInvokeFull Nothing 0 $ do
|
||||||
|
action
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
-- | Set _NET_WM_WINDOW_TYPE to _NET_WM_WINDOW_TYPE_DIALOG for a realized GTK window.
|
||||||
|
-- Safe to call from the GTK main thread after the window has a surface (i.e. after realize).
|
||||||
|
setWindowAsDialog :: Gtk.IsWindow w => w -> IO ()
|
||||||
|
setWindowAsDialog win = do
|
||||||
|
-- upcast to concrete Gtk.Window right away (resolves ambiguity)
|
||||||
|
wnd <- Gtk.toWindow win
|
||||||
|
|
||||||
|
-- now get surface with a concrete Window type in scope
|
||||||
|
mSurface <- #getSurface wnd
|
||||||
|
case mSurface of
|
||||||
|
Just (surface :: Gdk.Surface) -> do
|
||||||
|
-- cast to GDK X11 surface & display (explicit IO results)
|
||||||
|
mX11Surface <- (castTo GdkX11.X11Surface surface) :: IO (Maybe GdkX11.X11Surface)
|
||||||
|
display <- Gdk.surfaceGetDisplay surface
|
||||||
|
mX11Display <- (castTo GdkX11.X11Display display) :: IO (Maybe GdkX11.X11Display)
|
||||||
|
|
||||||
|
case (mX11Display, mX11Surface) of
|
||||||
|
(Just _gdkX11Disp, Just x11Surf) -> do
|
||||||
|
-- get window XID and set ATOM via X11
|
||||||
|
xidCULong <- GdkX11.x11SurfaceGetXid x11Surf
|
||||||
|
|
||||||
|
dpy <- openDisplay ""
|
||||||
|
atomType <- internAtom dpy "_NET_WM_WINDOW_TYPE" False
|
||||||
|
atomDialog <- internAtom dpy "_NET_WM_WINDOW_TYPE_DIALOG" False
|
||||||
|
atomAtom <- internAtom dpy "ATOM" False
|
||||||
|
|
||||||
|
withArray [atomDialog] $ \ptr -> do
|
||||||
|
let winX :: Window
|
||||||
|
winX = fromIntegral (xidCULong :: CULong)
|
||||||
|
void $ xChangeProperty dpy winX atomType atomAtom 32 propModeReplace (castPtr ptr) 1
|
||||||
|
flush dpy
|
||||||
|
|
||||||
|
closeDisplay dpy
|
||||||
|
_ -> putStrLn "Not running under X11."
|
||||||
|
Nothing -> putStrLn "No surface available"
|
||||||
|
|
||||||
|
-- | Attach setting the dialog atom to the window's #realize signal.
|
||||||
|
-- Returns the SignalHandlerId so you can disconnect if needed.
|
||||||
|
attachDialogOnRealize :: Gtk.IsWindow w => w -> IO SignalHandlerId
|
||||||
|
attachDialogOnRealize win = do
|
||||||
|
wnd <- Gtk.toWindow win
|
||||||
|
on wnd #realize (setWindowAsDialog win)
|
||||||
Loading…
Reference in a new issue