From 86aae0ebbb695703b809f8b8238527a02322b179 Mon Sep 17 00:00:00 2001 From: Ram Rachum Date: Sat, 1 Nov 2025 15:52:41 +0200 Subject: [PATCH 1/8] posh shawty: Handle leading slash correctly --- python_toolbox/poshing.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/python_toolbox/poshing.py b/python_toolbox/poshing.py index 197f4a3e..c2a84655 100644 --- a/python_toolbox/poshing.py +++ b/python_toolbox/poshing.py @@ -102,12 +102,22 @@ def _posh(path_string: str = None, allow_cwd: bool = True) -> str: def apply_shawty(path_string: str, shawty_length_threshold: int = 30) -> str: """Apply shawty abbreviation to a path string.""" + starts_with_slash = path_string.startswith('/') slash_count = path_string.count('/') - if slash_count < 2: + + # Adjust count if path starts with slash (leading slash doesn't count) + adjusted_count = slash_count - 1 if starts_with_slash else slash_count + + if adjusted_count < 2: return path_string # Find first and last slash positions - first_slash = path_string.index('/') + if starts_with_slash: + # Skip the leading slash, find the second slash + first_slash = path_string.index('/', 1) + else: + first_slash = path_string.index('/') + last_slash = path_string.rindex('/') # Build abbreviated path (without slashes around ellipsis) From 11c54a5e78944a7731570319ac79ad0bb0a952d2 Mon Sep 17 00:00:00 2001 From: Ram Rachum Date: Sat, 1 Nov 2025 15:52:51 +0200 Subject: [PATCH 2/8] Bump version to 1.2.10 --- docs/conf.py | 4 ++-- python_toolbox/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index aac3895b..2e339c0a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,9 +45,9 @@ # built documents. # # The short X.Y version. -version = '1.2.9' +version = '1.2.10' # The full version, including alpha/beta/rc tags. -release = '1.2.9' +release = '1.2.10' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/python_toolbox/__init__.py b/python_toolbox/__init__.py index 8863ab4b..5772c637 100644 --- a/python_toolbox/__init__.py +++ b/python_toolbox/__init__.py @@ -13,7 +13,7 @@ import python_toolbox.version_info -__version__ = '1.2.9' +__version__ = '1.2.10' __version_info__ = python_toolbox.version_info.VersionInfo( *(map(int, __version__.split('.'))) ) From 3a490a492884eaba3ad4c253381739d94ece6461 Mon Sep 17 00:00:00 2001 From: Ram Rachum Date: Mon, 17 Nov 2025 20:05:40 +0200 Subject: [PATCH 3/8] Remove pkg_resources and everything that uses it --- python_toolbox/wx_tools/__init__.py | 1 - python_toolbox/wx_tools/bitmap_tools.py | 35 --- python_toolbox/wx_tools/cursors/__init__.py | 1 - .../wx_tools/cursors/collection/__init__.py | 6 - .../wx_tools/cursors/collection/collection.py | 47 --- .../cursors/collection/images/__init__.py | 4 - .../cursors/collection/images/closed_grab.png | Bin 2887 -> 0 bytes .../cursors/collection/images/open_grab.png | Bin 2915 -> 0 bytes .../wx_tools/widgets/knob/__init__.py | 10 - .../wx_tools/widgets/knob/images/__init__.py | 4 - .../wx_tools/widgets/knob/images/knob.png | Bin 1171 -> 0 bytes python_toolbox/wx_tools/widgets/knob/knob.py | 287 ------------------ .../wx_tools/widgets/knob/snap_map.py | 208 ------------- 13 files changed, 603 deletions(-) delete mode 100644 python_toolbox/wx_tools/bitmap_tools.py delete mode 100644 python_toolbox/wx_tools/cursors/collection/__init__.py delete mode 100644 python_toolbox/wx_tools/cursors/collection/collection.py delete mode 100644 python_toolbox/wx_tools/cursors/collection/images/__init__.py delete mode 100644 python_toolbox/wx_tools/cursors/collection/images/closed_grab.png delete mode 100644 python_toolbox/wx_tools/cursors/collection/images/open_grab.png delete mode 100644 python_toolbox/wx_tools/widgets/knob/__init__.py delete mode 100644 python_toolbox/wx_tools/widgets/knob/images/__init__.py delete mode 100644 python_toolbox/wx_tools/widgets/knob/images/knob.png delete mode 100644 python_toolbox/wx_tools/widgets/knob/knob.py delete mode 100644 python_toolbox/wx_tools/widgets/knob/snap_map.py diff --git a/python_toolbox/wx_tools/__init__.py b/python_toolbox/wx_tools/__init__.py index 0f7ecf5c..be4990d4 100644 --- a/python_toolbox/wx_tools/__init__.py +++ b/python_toolbox/wx_tools/__init__.py @@ -14,7 +14,6 @@ from . import colors from . import keyboard from . import window_tools -from . import bitmap_tools from . import cursors from . import event_tools from . import generic_bitmaps diff --git a/python_toolbox/wx_tools/bitmap_tools.py b/python_toolbox/wx_tools/bitmap_tools.py deleted file mode 100644 index de515f8b..00000000 --- a/python_toolbox/wx_tools/bitmap_tools.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2009-2017 Ram Rachum. -# This program is distributed under the MIT license. - -'''Defines bitmap-related tools.''' - -import pkg_resources -import wx - - -def color_replaced_bitmap(bitmap, old_rgb, new_rgb): - '''Replace all appearances of `old_rgb` with `new_rgb` in `bitmap`.''' - old_r, old_g, old_b = old_rgb - new_r, new_g, new_b = new_rgb - image = wx.ImageFromBitmap(bitmap) - assert isinstance(image, wx.Image) - image.Replace(old_r, old_g, old_b, new_r, new_g, new_b) - return wx.BitmapFromImage(image) - - -def bitmap_from_pkg_resources(package_or_requirement, resource_name): - ''' - Get a bitmap from a file using `pkg_resources`. - - Example: - - my_bitmap = bitmap_from_pkg_resources('whatever.images', 'image.jpg') - - ''' - return wx.Bitmap( - wx.Image( - pkg_resources.resource_stream(package_or_requirement, - resource_name), - wx.BITMAP_TYPE_ANY - ) - ) \ No newline at end of file diff --git a/python_toolbox/wx_tools/cursors/__init__.py b/python_toolbox/wx_tools/cursors/__init__.py index 4dc84716..239f264b 100644 --- a/python_toolbox/wx_tools/cursors/__init__.py +++ b/python_toolbox/wx_tools/cursors/__init__.py @@ -3,5 +3,4 @@ '''Defines various cursor-related tools.''' -from . import collection from .cursor_changer import CursorChanger \ No newline at end of file diff --git a/python_toolbox/wx_tools/cursors/collection/__init__.py b/python_toolbox/wx_tools/cursors/collection/__init__.py deleted file mode 100644 index a0b160f7..00000000 --- a/python_toolbox/wx_tools/cursors/collection/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -# Copyright 2009-2017 Ram Rachum. -# This program is distributed under the MIT license. - -'''A collection of cursors.''' - -from .collection import get_open_grab, get_closed_grab \ No newline at end of file diff --git a/python_toolbox/wx_tools/cursors/collection/collection.py b/python_toolbox/wx_tools/cursors/collection/collection.py deleted file mode 100644 index 9447dd30..00000000 --- a/python_toolbox/wx_tools/cursors/collection/collection.py +++ /dev/null @@ -1,47 +0,0 @@ -# Copyright 2009-2017 Ram Rachum. -# This program is distributed under the MIT license. - -'''A collection of cursors.''' - -import pkg_resources -import wx - -from python_toolbox import caching - - -from . import images as __images_package -images_package = __images_package.__name__ - - -@caching.cache() -def get_open_grab(): - '''Get the "open grab" cursor.''' - file_name = 'open_grab.png' - hotspot = (8, 8) - stream = pkg_resources.resource_stream(images_package, - file_name) - image = wx.ImageFromStream(stream, wx.BITMAP_TYPE_ANY) - - if hotspot is not None: - image.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, hotspot[0]) - image.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, hotspot[1]) - - cursor = wx.CursorFromImage(image) - return cursor - - -@caching.cache() -def get_closed_grab(): - '''Get the "closed grab" cursor.''' - file_name = 'closed_grab.png' - hotspot = (8, 8) - stream = pkg_resources.resource_stream(images_package, - file_name) - image = wx.ImageFromStream(stream, wx.BITMAP_TYPE_ANY) - - if hotspot is not None: - image.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_X, hotspot[0]) - image.SetOptionInt(wx.IMAGE_OPTION_CUR_HOTSPOT_Y, hotspot[1]) - - cursor = wx.CursorFromImage(image) - return cursor diff --git a/python_toolbox/wx_tools/cursors/collection/images/__init__.py b/python_toolbox/wx_tools/cursors/collection/images/__init__.py deleted file mode 100644 index 41546a51..00000000 --- a/python_toolbox/wx_tools/cursors/collection/images/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright 2009-2017 Ram Rachum. -# This program is distributed under the MIT license. - -'''Images package.''' diff --git a/python_toolbox/wx_tools/cursors/collection/images/closed_grab.png b/python_toolbox/wx_tools/cursors/collection/images/closed_grab.png deleted file mode 100644 index 3e3262c59ab7099c8eb70d59c5da13018548704e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2887 zcmV-N3%K-&P)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=00004XF*Lt006O$eEU(80000WV@Og>004R=004l4008;_004mL004C` z008P>0026e000+nl3&F}0001DNkl#% diff --git a/python_toolbox/wx_tools/cursors/collection/images/open_grab.png b/python_toolbox/wx_tools/cursors/collection/images/open_grab.png deleted file mode 100644 index 3e051cbc8908876147df095727cf4d93dbd40333..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2915 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 self.angle_resolution: - self.current_angle = angle - self.Refresh() - self.needs_recalculation_flag = False - - def _on_paint(self, event): - '''EVT_PAINT handler.''' - - # Not checking for recalculation flag, this widget is not real-time - # enough to care about the delay. - - dc = wx.BufferedPaintDC(self) - - dc.SetBackground(wx_tools.colors.get_background_brush()) - dc.Clear() - - w, h = self.GetClientSize() - - gc = wx.GraphicsContext.Create(dc) - - gc.SetPen(wx.TRANSPARENT_PEN) - gc.SetBrush(self._knob_house_brush) - - assert isinstance(gc, wx.GraphicsContext) - gc.Translate(w/2, h/2) - gc.Rotate(self.current_angle) - gc.DrawEllipse(-13.5, -13.5, 27, 27) - gc.DrawBitmap(self.original_bitmap, -13, -13, 26, 26) - - #gc.DrawEllipse(5,5,2,2) - #gc.DrawEllipse(100,200,500,500) - - def _on_size(self, event): - '''EVT_SIZE handler.''' - event.Skip() - self.Refresh() - - def _on_mouse_events(self, event): - '''EVT_MOUSE_EVENTS handler.''' - # todo: maybe right click should give context menu with - # 'Sensitivity...' - # todo: make check: if left up and has capture, release capture - - self.Refresh() - - (w, h) = self.GetClientSize() - (x, y) = event.GetPositionTuple() - - - if event.LeftDown(): - self.being_dragged = True - self.snap_map = SnapMap( - snap_point_ratios=self._get_snap_points_as_ratios(), - base_drag_radius=self.base_drag_radius, - snap_point_drag_well=self.snap_point_drag_well, - initial_y=y, - initial_ratio=self.current_ratio - ) - - self.SetCursor(wx_tools.cursors.collection.get_closed_grab()) - # SetCursor must be before CaptureMouse because of wxPython/GTK - # weirdness - self.CaptureMouse() - - return - - if event.LeftIsDown() and self.HasCapture(): - ratio = self.snap_map.y_to_ratio(y) - value = self._ratio_to_value(ratio) - self.value_setter(value) - - - if event.LeftUp(): - # todo: make sure that when leaving - # entire app, things don't get fucked - if self.HasCapture(): - self.ReleaseMouse() - # SetCursor must be after ReleaseMouse because of wxPython/GTK - # weirdness - self.SetCursor(wx_tools.cursors.collection.get_open_grab()) - self.being_dragged = False - self.snap_map = None - - - return - - - - - diff --git a/python_toolbox/wx_tools/widgets/knob/snap_map.py b/python_toolbox/wx_tools/widgets/knob/snap_map.py deleted file mode 100644 index 6a0be432..00000000 --- a/python_toolbox/wx_tools/widgets/knob/snap_map.py +++ /dev/null @@ -1,208 +0,0 @@ -# Copyright 2009-2011 Ram Rachum. -# This program is distributed under the LGPL2.1 license. - -''' -Defines the `SnapMap` class. - -See its documentation for more info. -''' - -from __future__ import division -from python_toolbox import misc_tools - - -FUZZ = 0.001 -''' -The fuzziness of floating point numbers. - -If two floats have a distance of less than FUZZ, we may treat them as identical. -''' - - -class SnapMap: - ''' - Map for deciding which angle the knob will have when mouse-dragging. - - - Here we have three "scales" we are playing in: - - 1. The "ratio" scale. See documenation on Knob for that one. This controls - the angle of the knob and the actual value of the final variable. - - 2. The "y" scale. This is the `y` reading of the mouse on the screen. - - 3. The "pos" scale. This is a convenient mediator between the first two. It - is reversed from "y", because on the screen a higher number of y means - "down", and that's just wrong. Also, it has some translation. - - ''' - def __init__(self, snap_point_ratios, base_drag_radius, - snap_point_drag_well, initial_y, initial_ratio): - - assert snap_point_ratios == sorted(snap_point_ratios) - - self.snap_point_ratios = snap_point_ratios - '''Ordered list of snap points, as ratios.''' - - self.base_drag_radius = base_drag_radius - ''' - The base drag radius, in pixels. - - This number is the basis for calculating the height of the area in which - the user can play with the mouse to turn the knob. Beyond that area the - knob will be turned all the way to one side, and any movement farther - will have no effect. - - If there are no snap points, the total height of that area will be `2 * - self.base_drag_radius`. - ''' - - self.snap_point_drag_well = snap_point_drag_well - ''' - The height of a snap point's drag well, in pixels. - - This is the height of the area on the screen in which, when the user - drags to it, the knob will have the value of the snap point. - - The bigger this is, the harder the snap point "traps" the mouse. - ''' - - self.initial_y = initial_y - '''The y that was recorded when the user started dragging.''' - - self.initial_ratio = initial_ratio - '''The ratio that was recorded when the user started dragging.''' - - self.initial_pos = self.ratio_to_pos(initial_ratio) - '''The pos that was recorded when the user started dragging.''' - - self.max_pos = base_drag_radius * 2 + \ - len(snap_point_ratios) * snap_point_drag_well - '''The maximum that a pos number can reach before it gets truncated.''' - - self._make_snap_point_pos_starts() - - - ########################################################################### - # # # # Converters: - ############ - - def ratio_to_pos(self, ratio): - '''Convert from ratio to pos.''' - assert (- 1 - FUZZ) <= ratio <= 1 + FUZZ - n_snap_points_from_bottom = self._get_n_snap_points_from_bottom(ratio) - padding = n_snap_points_from_bottom * self.snap_point_drag_well - distance_from_bottom = ratio - (-1) - result = padding + distance_from_bottom * self.base_drag_radius - return result - - def pos_to_y(self, pos): - '''Convert from pos to y.''' - assert 0 - FUZZ <= pos <= self.max_pos + FUZZ - relative_pos = (pos - self.initial_pos) - return self.initial_y - relative_pos - # doing minus because y is upside down - - def y_to_pos(self, y): - '''Convert from y to pos.''' - relative_y = (y - self.initial_y) - - # doing minus because y is upside down - pos = self.initial_pos - relative_y - - if pos < 0: - pos = 0 - if pos > self.max_pos: - pos = self.max_pos - - return pos - - - def pos_to_ratio(self, pos): - '''Convert from pos to ratio.''' - assert 0 - FUZZ <= pos <= self.max_pos + FUZZ - - snap_point_pos_starts_from_bottom = [ - p for p in self.snap_point_pos_starts if p <= pos - ] - - padding = 0 - - if snap_point_pos_starts_from_bottom: - - candidate_for_current_snap_point = \ - snap_point_pos_starts_from_bottom[-1] - - distance_from_candidate = (pos - candidate_for_current_snap_point) - - if distance_from_candidate < self.snap_point_drag_well: - - # It IS the current snap point! - - snap_point_pos_starts_from_bottom.remove( - candidate_for_current_snap_point - ) - - padding += distance_from_candidate - - padding += \ - len(snap_point_pos_starts_from_bottom) * self.snap_point_drag_well - - - ratio = ((pos - padding) / self.base_drag_radius) - 1 - - assert (- 1 - FUZZ) <= ratio <= 1 + FUZZ - - return ratio - - - def ratio_to_y(self, ratio): - '''Convert from ratio to y.''' - return self.pos_to_y(self.ratio_to_pos(ratio)) - - def y_to_ratio(self, y): - '''Convert from y to ratio.''' - return self.pos_to_ratio(self.y_to_pos(y)) - - ########################################################################### - - def _get_n_snap_points_from_bottom(self, ratio): - '''Get the number of snap points whose ratio is lower than `ratio`.''' - raw_list = [s for s in self.snap_point_ratios - if -1 <= s <= (ratio + FUZZ)] - - if not raw_list: - return 0 - else: # len(raw_list) >= 1 - counter = 0 - counter += len(raw_list[:-1]) - last_snap_point = raw_list[-1] - ratio_in_last_snap_point = (abs(last_snap_point - ratio) < FUZZ) - if ratio_in_last_snap_point: - counter += 0.5 - else: - counter += 1 - return counter - - - def _make_snap_point_pos_starts(self): - ''' - Make a list with a "pos start" for each snap point. - - A "pos start" is the lowest point, in pos scale, of a snap point's drag - well. - - The list is not returned, but is stored as the attribute - `.snap_point_pos_starts`. - ''' - - self.snap_point_pos_starts = [] - - for i, ratio in enumerate(self.snap_point_ratios): - self.snap_point_pos_starts.append( - (1 + ratio) * self.base_drag_radius + \ - i * self.snap_point_drag_well - ) - - - From 4e9ccd3ff101afecbfa71bd62a84ad11902b4ec8 Mon Sep 17 00:00:00 2001 From: Ram Rachum Date: Mon, 17 Nov 2025 20:06:32 +0200 Subject: [PATCH 4/8] Bump version to 1.3.0 --- docs/conf.py | 4 ++-- python_toolbox/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 2e339c0a..35799e0f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,9 +45,9 @@ # built documents. # # The short X.Y version. -version = '1.2.10' +version = '1.3.0' # The full version, including alpha/beta/rc tags. -release = '1.2.10' +release = '1.3.0' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/python_toolbox/__init__.py b/python_toolbox/__init__.py index 5772c637..6099b147 100644 --- a/python_toolbox/__init__.py +++ b/python_toolbox/__init__.py @@ -13,7 +13,7 @@ import python_toolbox.version_info -__version__ = '1.2.10' +__version__ = '1.3.0' __version_info__ = python_toolbox.version_info.VersionInfo( *(map(int, __version__.split('.'))) ) From b9304ab5599f096147fe96e835d12d538b84f6cd Mon Sep 17 00:00:00 2001 From: Ram Rachum Date: Thu, 20 Nov 2025 19:21:27 +0200 Subject: [PATCH 5/8] Update poshing.py to use config.json instead of envvars.json, support shawty_length_threshold from config --- python_toolbox/poshing.py | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/python_toolbox/poshing.py b/python_toolbox/poshing.py index c2a84655..d40b7d76 100644 --- a/python_toolbox/poshing.py +++ b/python_toolbox/poshing.py @@ -24,28 +24,37 @@ def format_envvar(x: str) -> str: return '~' if x == 'HOME' else f'${x}' -def load_envvar_paths() -> dict: +def load_config() -> tuple[dict, int]: """ - Load environment variable paths from ~/.posh/envvars.json. - Returns an empty dict if the file doesn't exist or is empty. + Load configuration from ~/.posh/config.json. + Returns (envvar_paths_dict, shawty_length_threshold). Expected format: { - "ENVVAR_NAME": ["path1", "path2", ...], - ... + "envvars": { + "ENVVAR_NAME": ["path1", "path2", ...], + ... + }, + "shawty_length_threshold": 30 } """ - config_path = pathlib.Path.home() / '.posh' / 'envvars.json' + config_path = pathlib.Path.home() / '.posh' / 'config.json' if not config_path.exists(): - return {} + return {}, 30 try: with open(config_path, 'r') as f: data = json.load(f) - return data if isinstance(data, dict) else {} + if not isinstance(data, dict): + return {}, 30 + + envvars = data.get('envvars', {}) + threshold = data.get('shawty_length_threshold', 30) + + return (envvars if isinstance(envvars, dict) else {}), threshold except (json.JSONDecodeError, IOError): - return {} + return {}, 30 def _posh(path_string: str = None, allow_cwd: bool = True) -> str: @@ -70,7 +79,7 @@ def _posh(path_string: str = None, allow_cwd: bool = True) -> str: # Load envvar paths from config file - envvar_paths = load_envvar_paths() + envvar_paths, _ = load_config() # Convert string paths to pathlib.Path objects for envvar_name in list(envvar_paths.keys()): @@ -135,7 +144,7 @@ def posh(path_strings: Iterable[str] | str | None = None, separator: str = SEPARATOR_NEWLINE, allow_cwd: bool = True, shawty: bool = False, - shawty_length_threshold: int = 30) -> str: + shawty_length_threshold: int | None = None) -> str: """ Convert paths to a more readable format using environment variables. @@ -145,7 +154,7 @@ def posh(path_strings: Iterable[str] | str | None = None, separator: Separator to use between multiple paths (SEPARATOR_NEWLINE or SEPARATOR_SPACE) allow_cwd: When False, don't resolve relative paths against current working directory shawty: Abbreviate paths with 2+ slashes: replace middle sections with ellipsis - shawty_length_threshold: If abbreviated path still exceeds this length, trim further + shawty_length_threshold: If abbreviated path still exceeds this length, trim further (defaults to config.json value) Returns: Formatted path string(s) @@ -156,10 +165,14 @@ def posh(path_strings: Iterable[str] | str | None = None, if not isinstance(path_strings, (list, tuple)): path_strings = [path_strings] + # Load config to get default threshold if not provided + _, config_threshold = load_config() + threshold = shawty_length_threshold if shawty_length_threshold is not None else config_threshold + results = [_posh(path_string, allow_cwd=allow_cwd) for path_string in path_strings] if shawty: - results = [apply_shawty(result, shawty_length_threshold) for result in results] + results = [apply_shawty(result, threshold) for result in results] if quote_mode == QUOTE_ALWAYS: quoted_results = [f'"{result}"' for result in results] From 091aac543a09902fe28d5c1637fb859a49e4017a Mon Sep 17 00:00:00 2001 From: Ram Rachum Date: Thu, 20 Nov 2025 19:23:47 +0200 Subject: [PATCH 6/8] Bump version to 1.3.1 --- docs/conf.py | 4 ++-- python_toolbox/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 35799e0f..ec58badc 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -45,9 +45,9 @@ # built documents. # # The short X.Y version. -version = '1.3.0' +version = '1.3.1' # The full version, including alpha/beta/rc tags. -release = '1.3.0' +release = '1.3.1' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/python_toolbox/__init__.py b/python_toolbox/__init__.py index 6099b147..4e59e5af 100644 --- a/python_toolbox/__init__.py +++ b/python_toolbox/__init__.py @@ -13,7 +13,7 @@ import python_toolbox.version_info -__version__ = '1.3.0' +__version__ = '1.3.1' __version_info__ = python_toolbox.version_info.VersionInfo( *(map(int, __version__.split('.'))) ) From 110d567d4f73ec382f0524464dbbcc18185f17e0 Mon Sep 17 00:00:00 2001 From: Ram Rachum Date: Mon, 1 Dec 2025 22:45:35 +0200 Subject: [PATCH 7/8] poshing: Add envvar expansion and path separator normalization in config values --- python_toolbox/poshing.py | 108 ++++++++++++++++++++++++++++---------- 1 file changed, 81 insertions(+), 27 deletions(-) diff --git a/python_toolbox/poshing.py b/python_toolbox/poshing.py index d40b7d76..b9a73c0d 100644 --- a/python_toolbox/poshing.py +++ b/python_toolbox/poshing.py @@ -24,6 +24,82 @@ def format_envvar(x: str) -> str: return '~' if x == 'HOME' else f'${x}' +def expand_envvars(s: str) -> str: + """Expand environment variables in a string. Supports $VAR and ${VAR} syntax.""" + result = s + + # Handle ${VAR} syntax + result = re.sub( + r'\$\{([^}]+)\}', + lambda m: os.environ.get(m.group(1), m.group(0)), + result + ) + + # Handle $VAR syntax (variable name ends at non-alphanumeric/underscore) + result = re.sub( + r'\$([A-Za-z_][A-Za-z0-9_]*)', + lambda m: os.environ.get(m.group(1), m.group(0)), + result + ) + + return result + + +def ensure_windows_path_string(path_string: str) -> str: + """Convert Linux/POSIX-style paths to Windows format.""" + # Handle file:/// URLs + if path_string.startswith('file:///'): + return urllib.parse.unquote(path_string[8:]) + + # Return other URLs (like http://) unaltered + if re.match(r'^[a-zA-Z]+://', path_string): + return path_string + + path = pathlib.Path(path_string) + posix_path = path.as_posix() + if re.match('^/[a-zA-Z]/.*$', posix_path): + # Handle local drive paths like /c/Users/... + return '%s:%s' % ( + posix_path[1], + re.sub('(?<=[^\\\\])\\\\ ', ' ', posix_path).replace('/', '\\')[2:] + ) + elif re.match('^//[^/]+/.*$', posix_path): + # Handle UNC network paths like //server/share/... + return posix_path.replace('/', '\\') + else: + return path_string + + +def normalize_path_separators(s: str) -> str: + """Normalize path separators for the current OS.""" + if sys.platform == 'win32': + return ensure_windows_path_string(s) + else: + return ensure_linux_path_string(s) + + +def ensure_linux_path_string(path_string: str) -> str: + """Convert Windows-style paths to Linux/POSIX format.""" + # Handle file:/// URLs + if path_string.startswith('file:///'): + return urllib.parse.unquote(path_string[8:]) + + # Return other URLs (like http://) unaltered + if re.match(r'^[a-zA-Z]+://', path_string): + return path_string + + # Convert backslashes to forward slashes + result = path_string.replace('\\', '/') + + # Handle Windows drive paths like C:/Users/... -> /c/Users/... + if re.match(r'^[a-zA-Z]:/', result): + result = '/' + result[0].lower() + result[2:] + + # Handle UNC paths like //server/share -> //server/share (already correct) + + return result + + def load_config() -> tuple[dict, int]: """ Load configuration from ~/.posh/config.json. @@ -81,11 +157,14 @@ def _posh(path_string: str = None, allow_cwd: bool = True) -> str: # Load envvar paths from config file envvar_paths, _ = load_config() - # Convert string paths to pathlib.Path objects + # Convert string paths to pathlib.Path objects (with envvar expansion and separator normalization) for envvar_name in list(envvar_paths.keys()): if not isinstance(envvar_paths[envvar_name], list): envvar_paths[envvar_name] = [] - envvar_paths[envvar_name] = [pathlib.Path(p) for p in envvar_paths[envvar_name]] + envvar_paths[envvar_name] = [ + pathlib.Path(normalize_path_separators(expand_envvars(p))) + for p in envvar_paths[envvar_name] + ] # Add environment values if they exist for envvar_name in envvar_paths: @@ -190,31 +269,6 @@ def posh(path_strings: Iterable[str] | str | None = None, return sep.join(quoted_results) -def ensure_windows_path_string(path_string: str) -> str: - # Handle file:/// URLs - if path_string.startswith('file:///'): - # Strip the file:/// prefix and decode URL encoding - return urllib.parse.unquote(path_string[8:]) - - # Return other URLs (like http://) unaltered - if re.match(r'^[a-zA-Z]+://', path_string): - return path_string - - path = pathlib.Path(path_string) - posix_path = path.as_posix() - if re.match('^/[a-zA-Z]/.*$', posix_path): - # Handle local drive paths like /c/Users/... - return '%s:%s' % ( - posix_path[1], - re.sub('(?<=[^\\\\])\\\\ ', ' ', posix_path).replace('/', '\\')[2:] - ) - elif re.match('^//[^/]+/.*$', posix_path): - # Handle UNC network paths like //server/share/... - return posix_path.replace('/', '\\') - else: - return path_string - - def posh_path(path: pathlib.Path | str, allow_cwd: bool = True) -> str: """Process a path using the posh function directly.""" path_str = str(path) From ff10bcd19b7747b615f6db1c44460b9f5eed6d9a Mon Sep 17 00:00:00 2001 From: Ram Rachum Date: Mon, 1 Dec 2025 22:45:51 +0200 Subject: [PATCH 8/8] Bump version to 1.3.2 --- python_toolbox/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python_toolbox/__init__.py b/python_toolbox/__init__.py index 4e59e5af..c5262afa 100644 --- a/python_toolbox/__init__.py +++ b/python_toolbox/__init__.py @@ -13,7 +13,7 @@ import python_toolbox.version_info -__version__ = '1.3.1' +__version__ = '1.3.2' __version_info__ = python_toolbox.version_info.VersionInfo( *(map(int, __version__.split('.'))) )